diff --git a/.gitignore b/.gitignore index 7d17454018..1391d1b041 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Docs/API # temp nibs and swap files *~.nib *.swp +*.orig # OS X folder attributes .DS_Store @@ -20,6 +21,4 @@ Docs/API Examples/RKDiscussionBoardExample/discussion_board_backend/public/system/attachments/* -# UISpecRunner cached build path -.uispec.app -Specs/Runner/UISpec +test-reports/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 5042ac3d8b..50fe0d83eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "Examples/RKCatalog/Server"] path = Examples/RKCatalog/Server - url = git://github.com/twotoasters/RKCatalog-Server.git + url = git://github.com/RestKit/RKCatalog-Server.git diff --git a/CREDITS.md b/CREDITS.md index 151a0b31be..06bc559607 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -6,32 +6,79 @@ as a Ruby on Rails specific object mapper for XML data. In early 2010 the framew rebranded as RestKit and evolved into a general purpose HTTP toolkit and object mapping system. -RestKit is a production of Two Toasters and available as an Open Source package under -the terms of the Apache License (see LICENSE for details). - -Original Author ---------------- -* Blake Watters (blakewatters) @blakewatters - -Core Team ---------- -* Jeremy Ellison (jeremyellison) -* Daniel Hammond (danielrhammond) -* Jeff Arena (jeffarena) - -Web Designer ------------- -* Adit Shukla (aditshukla) - -Contributors ------------- -* Marc Weil (aspir) -* Pat Shields (pashields) -* Tim Kerchmar (timkerchmar) -* Rachit Shukla (rachitshukla) -* Adam Hinz (ahinz) -* Stefan Eletzhofer (seletz) -* Peter Marks (tassock) -* Chad Podoski (chadpod) -* Andras Hatvani (andrashatvani) -* Ed McManus (emcmanus) +RestKit is available as an Open Source package under the terms of the Apache License (see +LICENSE for details). + +## Original Author +* Blake Watters (blakewatters) @blakewatters + +## Core Team +* Jeff Arena (jeffarena) +* Gregory Combs (grgcombs) +* Brian Morton (bmorton) + +## Web Designer +* Adit Shukla (aditshukla) + +## Contributors +#### Version 0.9.3 and earlier +* Jeremy Ellison (jeremyellison) +* Daniel Hammond (danielrhammond) +* Marc Weil (aspir) +* Pat Shields (pashields) +* Tim Kerchmar (timkerchmar) +* Rachit Shukla (rachitshukla) +* Adam Hinz (ahinz) +* Stefan Eletzhofer (seletz) +* Peter Marks (tassock) +* Chad Podoski (chadpod) +* Andras Hatvani (andrashatvani) +* Ed McManus (emcmanus) + +#### Version 0.10.0 +* Christopher Swasey (endash) +* Aneil Mallavarapu (amallavarapu) +* Rui D Lopes (ruidlopes) +* Robert Altman (inquinity) +* Beat Besmer (besi) +* Scott Penrose (spenrose) +* Charlie Savage (cfis) +* Jawwad Ahmad (jawwad) +* John Stallings (jstallings) +* Bob Spryn (sprynmr) +* Ray Fix (rayfix) +* Marlon Andrade (marlonandrade) +* David Young-Chan Kay (DavidYKay) +* Chethan Reddy (creddy) +* Julien Grimault (juliengrimault) +* Matthias Bartelmeß +* Nolan Waite (nolanw) +* Michael Fleet (fantasticmf) +* Tony Lee (hktonylee) +* Aaron Crespo (aaroncrespo) +* James Sullivan (jsullivanlive) +* Marco Pesenti Gritti (marcopg) +* Brad Phelan (bradphelan) +* Ivan Vučica (ivucica) +* Felix Holmgren (Felixyz) +* Open Thread (OpenFibers) +* Sergej Tatarincev (SevInf) +* Ben Einstein (beinstein) +* Johan Bilien (jobi) +* Björn Jonsson (bjornjonsson) +* Ralf van der Zanden (ralfvdz) +* Parker Boundy (parkerboundy) +* Jeremy Mack (mutewinter) +* Allen Wei (allenwei) +* Robin Eggenkamp (Edubits) +* Emil Wojtaszek (emilwojtaszek) +* Victor Kryukov (victorkryukov) +* Cody Rayment (crayment) +* Arne Harren (aharren) +* Cameron Royal (cammm) + +## Honorable Mentions +RestKit would like to thank the following companies for aiding in the support of this product: + +* GateGuru - http://www.gateguru.com +* Two Toasters - http://www.twotoasters.com \ No newline at end of file diff --git a/Code/CoreData/CoreData.h b/Code/CoreData/CoreData.h index c52fae3d8d..592f8d7b0f 100644 --- a/Code/CoreData/CoreData.h +++ b/Code/CoreData/CoreData.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/30/10. -// Copyright 2010 Two Toasters -// +// 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,6 +23,16 @@ #import "NSManagedObject+ActiveRecord.h" #import "RKManagedObjectStore.h" #import "RKManagedObjectSeeder.h" -#import "RKManagedObjectCache.h" #import "RKManagedObjectMapping.h" +#import "RKManagedObjectMappingOperation.h" +#import "RKManagedObjectCaching.h" +#import "RKInMemoryManagedObjectCache.h" +#import "RKFetchRequestManagedObjectCache.h" +#import "RKSearchableManagedObject.h" +#import "RKSearchWord.h" + #import "RKObjectPropertyInspector+CoreData.h" +#import "RKObjectMappingProvider+CoreData.h" +#import "NSManagedObjectContext+RKAdditions.h" +#import "NSManagedObject+RKAdditions.h" +#import "NSEntityDescription+RKAdditions.h" diff --git a/Code/CoreData/NSEntityDescription+RKAdditions.h b/Code/CoreData/NSEntityDescription+RKAdditions.h new file mode 100644 index 0000000000..5da928b455 --- /dev/null +++ b/Code/CoreData/NSEntityDescription+RKAdditions.h @@ -0,0 +1,93 @@ +// +// NSEntityDescription+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 3/22/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +/** + The key for retrieving the name of the attribute that acts as + the primary key from the user info dictionary of the receiving NSEntityDescription. + + **Value**: @"primaryKeyAttribute" + */ +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. + */ +@interface NSEntityDescription (RKAdditions) + +/** + The name of the attribute that acts as the primary key for the receiver. + + The primary key attribute can be configured in two ways: + 1. From within the Xcode Core Data editing view by + adding the desired attribute's name as the value for the + key `primaryKeyAttribute` to the user info dictionary. + 1. Programmatically, by retrieving the NSEntityDescription instance and + setting the property's value. + + Programmatically configured values take precedence over the user info + dictionary. + */ +@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 new file mode 100644 index 0000000000..c244f4aeee --- /dev/null +++ b/Code/CoreData/NSEntityDescription+RKAdditions.m @@ -0,0 +1,107 @@ +// +// NSEntityDescription+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 3/22/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "NSEntityDescription+RKAdditions.h" + +NSString * const RKEntityDescriptionPrimaryKeyAttributeUserInfoKey = @"primaryKeyAttribute"; +NSString * const RKEntityDescriptionPrimaryKeyAttributeValuePredicateSubstitutionVariable = @"PRIMARY_KEY_VALUE"; + +static char primaryKeyAttributeNameKey, primaryKeyPredicateKey; + +@implementation NSEntityDescription (RKAdditions) + +- (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, &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)setPrimaryKeyAttributeName:(NSString *)primaryKeyAttributeName +{ + objc_setAssociatedObject(self, + &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 ef170f5fe3..1d05f63e15 100644 --- a/Code/CoreData/NSManagedObject+ActiveRecord.h +++ b/Code/CoreData/NSManagedObject+ActiveRecord.h @@ -1,5 +1,5 @@ // -// RKManagedObject+ActiveRecord.h +// NSManagedObject+ActiveRecord.h // // Adapted from https://github.com/magicalpanda/MagicalRecord // Created by Saul Mora on 11/15/09. @@ -10,22 +10,31 @@ #import -@interface NSManagedObject (ActiveRecord) +/** + Extensions to NSManagedObjectContext for RestKit's Active Record pattern implementation + */ +@interface NSManagedObjectContext (ActiveRecord) + ++ (NSManagedObjectContext *)defaultContext; ++ (void)setDefaultContext:(NSManagedObjectContext *)context; ++ (NSManagedObjectContext *)contextForCurrentThread; + +@end /** - * The Core Data managed object context from the RKObjectManager's objectStore - * that is managing this model + Provides extensions to NSManagedObject implementing a low-ceremony querying + interface. */ -+ (NSManagedObjectContext*)managedObjectContext; +@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; @@ -34,6 +43,12 @@ */ + (NSArray*)objectsWithFetchRequest:(NSFetchRequest*)fetchRequest; +/** + * Retrieves the number of objects that would be retrieved by the fetchRequest, + * if executed + */ ++ (NSUInteger)countOfObjectsWithFetchRequest:(NSFetchRequest*)fetchRequest; + /** * Fetches all objects from the persistent store via a set of fetch requests and * returns all results in a single array. @@ -79,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; @@ -88,6 +103,25 @@ */ - (BOOL)isNew; +/** + Finds the instance of the receiver's entity with the given value for the primary key attribute + in the managed object context for the current thread. + + @param primaryKeyValue The value for the receiving entity's primary key attribute. + @return The object with the primary key attribute equal to the given value or nil. + */ ++ (id)findByPrimaryKey:(id)primaryKeyValue; + +/** + Finds the instance of the receiver's entity with the given value for the primary key attribute in + the given managed object context. + + @param primaryKeyValue The value for the receiving entity's primary key attribute. + @param context The managed object context to find the instance in. + @return The object with the primary key attribute equal to the given value or nil. + */ ++ (id)findByPrimaryKey:(id)primaryKeyValue inContext:(NSManagedObjectContext *)context; + //////////////////////////////////////////////////////////////////////////////////////////////////// + (NSManagedObjectContext*)currentContext; diff --git a/Code/CoreData/NSManagedObject+ActiveRecord.m b/Code/CoreData/NSManagedObject+ActiveRecord.m index fb09c4e479..a246f020ee 100644 --- a/Code/CoreData/NSManagedObject+ActiveRecord.m +++ b/Code/CoreData/NSManagedObject+ActiveRecord.m @@ -10,11 +10,10 @@ #import #import "NSManagedObject+ActiveRecord.h" -#import "RKObjectManager.h" +#import "RKManagedObjectStore.h" #import "RKLog.h" #import "RKFixCategoryBug.h" - -RK_FIX_CATEGORY_BUG(NSManagedObject_ActiveRecord) +#import "NSEntityDescription+RKAdditions.h" // Set Logging Component #undef RKLogComponent @@ -23,86 +22,114 @@ static NSUInteger const kActiveRecordDefaultBatchSize = 10; static NSNumber *defaultBatchSize = nil; -@implementation NSManagedObject (ActiveRecord) +static NSManagedObjectContext *defaultContext = nil; -#pragma mark - RKManagedObject methods +RK_FIX_CATEGORY_BUG(NSManagedObjectContext_ActiveRecord) -+ (NSManagedObjectContext*)managedObjectContext { - NSAssert([RKObjectManager sharedManager], @"[RKObjectManager sharedManager] cannot be nil"); - NSAssert([RKObjectManager sharedManager].objectStore, @"[RKObjectManager sharedManager].objectStore cannot be nil"); - return [[[RKObjectManager sharedManager] objectStore] managedObjectContext]; +@implementation NSManagedObjectContext (ActiveRecord) + ++ (NSManagedObjectContext *)defaultContext { + return defaultContext; +} + ++ (void)setDefaultContext:(NSManagedObjectContext *)newDefaultContext { + [newDefaultContext retain]; + [defaultContext release]; + defaultContext = newDefaultContext; } ++ (NSManagedObjectContext *)contextForCurrentThread { + NSAssert([RKManagedObjectStore defaultObjectStore], @"[RKManagedObjectStore defaultObjectStore] cannot be nil"); + return [[RKManagedObjectStore defaultObjectStore] managedObjectContextForCurrentThread]; +} + +@end + +RK_FIX_CATEGORY_BUG(NSManagedObject_ActiveRecord) + +@implementation NSManagedObject (ActiveRecord) + +#pragma mark - RKManagedObject methods + + (NSEntityDescription*)entity { - NSString* className = [NSString stringWithCString:class_getName([self class]) encoding:NSASCIIStringEncoding]; - return [NSEntityDescription entityForName:className inManagedObjectContext:[self managedObjectContext]]; + 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 = [[self managedObjectContext] 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; } + (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 [[self managedObjectContext] 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:[self managedObjectContext]]; - return [object autorelease]; + id object = [[self alloc] initWithEntity:[self entity] insertIntoManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]]; + return [object autorelease]; } - (BOOL)isNew { @@ -110,104 +137,113 @@ - (BOOL)isNew { return [vals count] == 0; } ++ (id)findByPrimaryKey:(id)primaryKeyValue inContext:(NSManagedObjectContext *)context { + 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 findFirstWithPredicate:predicate inContext:context]; +} + ++ (id)findByPrimaryKey:(id)primaryKeyValue { + return [self findByPrimaryKey:primaryKeyValue inContext:[NSManagedObjectContext contextForCurrentThread]]; +} + #pragma mark - MagicalRecord Ported Methods + (NSManagedObjectContext*)currentContext; { - return [self managedObjectContext]; + return [NSManagedObjectContext contextForCurrentThread]; } + (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]); - } -} - -//- (void)handleErrors:(NSError *)error -//{ -// [[self class] handleErrors: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]); + } +} + (NSArray *)executeFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context { - NSError *error = nil; - - NSArray *results = [context executeFetchRequest:request error:&error]; - [self handleErrors:error]; - return results; + NSError *error = nil; + + NSArray *results = [context executeFetchRequest:request error:&error]; + [self handleErrors:error]; + return results; } -+ (NSArray *)executeFetchRequest:(NSFetchRequest *)request ++ (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]; - - NSArray *results = [self executeFetchRequest:request inContext:context]; - if ([results count] == 0) - { - return nil; - } - return [results objectAtIndex:0]; + [request setFetchLimit:1]; + + 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 @@ -219,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]]; - - return request; + NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; + [request setEntity:[self entityDescriptionInContext:context]]; + + return request; } + (NSFetchRequest *)createFetchRequest { - return [self createFetchRequestInContext:[self currentContext]]; + return [self createFetchRequestInContext:[self currentContext]]; } #pragma mark - @@ -304,34 +340,34 @@ + (NSFetchRequest *)createFetchRequest + (NSNumber *)numberOfEntitiesWithContext:(NSManagedObjectContext *)context { - NSError *error = nil; - NSUInteger count = [context countForFetchRequest:[self createFetchRequestInContext:context] error:&error]; - [self handleErrors:error]; - - return [NSNumber numberWithUnsignedInteger:count]; + NSError *error = nil; + NSUInteger count = [context countForFetchRequest:[self createFetchRequestInContext:context] error:&error]; + [self handleErrors:error]; + + 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]; - - NSUInteger count = [context countForFetchRequest:request error:&error]; - [self handleErrors:error]; - - return [NSNumber numberWithUnsignedInteger:count]; + NSError *error = nil; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; + + NSUInteger count = [context countForFetchRequest:request error:&error]; + [self handleErrors:error]; + + 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 @@ -348,19 +384,19 @@ + (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 { NSFetchRequest *request = [self createFetchRequestInContext:context]; [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", property, value]]; - + return request; } @@ -374,7 +410,7 @@ + (NSFetchRequest *)requestFirstWithPredicate:(NSPredicate *)searchTerm inContex NSFetchRequest *request = [self createFetchRequestInContext:context]; [request setPredicate:searchTerm]; [request setFetchLimit:1]; - + return request; } @@ -386,9 +422,8 @@ + (NSFetchRequest *)requestFirstWithPredicate:(NSPredicate *)searchTerm + (NSFetchRequest *)requestFirstByAttribute:(NSString *)attribute withValue:(id)searchValue inContext:(NSManagedObjectContext *)context; { NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPropertiesToFetch:[self propertiesNamed:[NSArray arrayWithObject:attribute]]]; [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]]; - + return request; } @@ -399,45 +434,45 @@ + (NSFetchRequest *)requestFirstByAttribute:(NSString *)attribute withValue:(id) + (NSFetchRequest *)requestAllSortedBy:(NSString *)sortTerm ascending:(BOOL)ascending inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self requestAllInContext:context]; - - NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; - [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; - [sortBy release]; - - return request; + NSFetchRequest *request = [self requestAllInContext:context]; + + NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; + [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; + [sortBy release]; + + 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]]; - - if (sortTerm != nil){ - NSSortDescriptor *sortBy = [[NSSortDescriptor alloc] initWithKey:sortTerm ascending:ascending]; - [request setSortDescriptors:[NSArray arrayWithObject:sortBy]]; - [sortBy release]; - } - - return request; + 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]; + } + + 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; } @@ -446,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]; - - return [self executeFetchRequest:request inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending 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]; - - return [self executeFetchRequest:request inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm + ascending:ascending + withPredicate:searchTerm + 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 - @@ -493,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]; - - NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request - managedObjectContext:context - sectionNameKeyPath:group - cacheName:cacheName]; - return [controller autorelease]; + + 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 *)fetchRequestAllGroupedBy:(NSString *)group withPredicate:(NSPredicate *)searchTerm sortedBy:(NSString *)sortTerm ascending:(BOOL)ascending ++ (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]; - - [self performFetch:controller]; - return controller; + NSFetchedResultsController *controller = [self fetchRequestAllGroupedBy:groupingKeyPath + withPredicate:searchTerm + sortedBy:sortTerm + ascending:ascending + inContext:context]; + + [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 @@ -567,50 +602,49 @@ + (NSFetchedResultsController *)fetchRequest:(NSFetchRequest *)request groupedBy + (NSArray *)findAllWithPredicate:(NSPredicate *)searchTerm inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [self createFetchRequestInContext:context]; - [request setPredicate:searchTerm]; - - return [self executeFetchRequest:request - inContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; + + 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]; - - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext: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]; - [request setPropertiesToFetch:[self propertiesNamed:[NSArray arrayWithObject:attribute]]]; - - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; +{ + NSFetchRequest *request = [self requestFirstByAttribute:attribute withValue:searchValue 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 { NSFetchRequest *request = [self requestFirstWithPredicate:searchTerm]; - + return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; } @@ -621,87 +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]; - - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:property ascending:ascending withPredicate:searchterm 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]; - [request setPropertiesToFetch:[self propertiesNamed:attributes]]; - - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + [request setPredicate:searchTerm]; + + 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]; - [request setPropertiesToFetch:[self propertiesNamed:attributes]]; - - return [self executeFetchRequestAndReturnFirstObject:request inContext:context]; + NSFetchRequest *request = [self requestAllSortedBy:sortBy + ascending:ascending + withPredicate:searchTerm + 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]; - - [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]]; - - return [self executeFetchRequest:request inContext:context]; + NSFetchRequest *request = [self createFetchRequestInContext:context]; + + [request setPredicate:[NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]]; + + 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]; - - return [self executeFetchRequest:request]; + NSPredicate *searchTerm = [NSPredicate predicateWithFormat:@"%K = %@", attribute, searchValue]; + NSFetchRequest *request = [self requestAllSortedBy:sortTerm ascending:ascending withPredicate:searchTerm inContext:context]; + + 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 @@ -711,22 +743,22 @@ + (id)createInContext:(NSManagedObjectContext *)context } + (id)createEntity -{ - NSManagedObject *newEntity = [self createInContext:[self currentContext]]; - - return newEntity; +{ + NSManagedObject *newEntity = [self createInContext:[self currentContext]]; + + 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 @@ -747,25 +779,25 @@ + (BOOL)truncateAll + (NSNumber *)maxValueFor:(NSString *)property { - NSManagedObject *obj = [[self class] findFirstByAttribute:property - withValue:[NSString stringWithFormat:@"max(%@)", property]]; - - return [obj valueForKey:property]; + NSManagedObject *obj = [[self class] findFirstByAttribute:property + withValue:[NSString stringWithFormat:@"max(%@)", property]]; + + return [obj valueForKey:property]; } + (id)objectWithMinValueFor:(NSString *)property inContext:(NSManagedObjectContext *)context { - NSFetchRequest *request = [[self class] createFetchRequestInContext:context]; - - NSPredicate *searchFor = [NSPredicate predicateWithFormat:@"SELF = %@ AND %K = min(%@)", self, property, property]; - [request setPredicate:searchFor]; - - return [[self class] executeFetchRequestAndReturnFirstObject:request inContext:context]; + NSFetchRequest *request = [[self class] createFetchRequestInContext:context]; + + NSPredicate *searchFor = [NSPredicate predicateWithFormat:@"SELF = %@ AND %K = min(%@)", self, property, property]; + [request setPredicate:searchFor]; + + return [[self class] executeFetchRequestAndReturnFirstObject:request inContext:context]; } -+ (id)objectWithMinValueFor:(NSString *)property ++ (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/NSManagedObject+RKAdditions.h b/Code/CoreData/NSManagedObject+RKAdditions.h new file mode 100644 index 0000000000..ef396736bd --- /dev/null +++ b/Code/CoreData/NSManagedObject+RKAdditions.h @@ -0,0 +1,23 @@ +// +// NSManagedObject+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 3/14/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +@class RKManagedObjectStore, RKManagedObjectMapping; + +/** + Provides extensions to NSManagedObject for various common tasks. + */ +@interface NSManagedObject (RKAdditions) + +/** + The receiver's managed object store. + */ +- (RKManagedObjectStore *)managedObjectStore; + +@end diff --git a/Code/CoreData/NSManagedObject+RKAdditions.m b/Code/CoreData/NSManagedObject+RKAdditions.m new file mode 100644 index 0000000000..58f1b4ce46 --- /dev/null +++ b/Code/CoreData/NSManagedObject+RKAdditions.m @@ -0,0 +1,19 @@ +// +// NSManagedObject+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 3/14/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "NSManagedObject+RKAdditions.h" +#import "NSManagedObjectContext+RKAdditions.h" + +@implementation NSManagedObject (RKAdditions) + +- (RKManagedObjectStore *)managedObjectStore +{ + return self.managedObjectContext.managedObjectStore; +} + +@end diff --git a/Code/CoreData/NSManagedObjectContext+RKAdditions.h b/Code/CoreData/NSManagedObjectContext+RKAdditions.h new file mode 100644 index 0000000000..2d1a4fe1c4 --- /dev/null +++ b/Code/CoreData/NSManagedObjectContext+RKAdditions.h @@ -0,0 +1,23 @@ +// +// NSManagedObjectContext+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 3/14/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +@class RKManagedObjectStore; + +/** + Provides extensions to NSManagedObjectContext for various common tasks. + */ +@interface NSManagedObjectContext (RKAdditions) + +/** + The receiver's managed object store. + */ +@property (nonatomic, assign) RKManagedObjectStore *managedObjectStore; + +@end diff --git a/Code/CoreData/NSManagedObjectContext+RKAdditions.m b/Code/CoreData/NSManagedObjectContext+RKAdditions.m new file mode 100644 index 0000000000..f838cd1db4 --- /dev/null +++ b/Code/CoreData/NSManagedObjectContext+RKAdditions.m @@ -0,0 +1,26 @@ +// +// NSManagedObjectContext+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 3/14/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "NSManagedObjectContext+RKAdditions.h" + +static char NSManagedObject_RKManagedObjectStoreAssociatedKey; + +@implementation NSManagedObjectContext (RKAdditions) + +- (RKManagedObjectStore *)managedObjectStore +{ + return (RKManagedObjectStore *) objc_getAssociatedObject(self, &NSManagedObject_RKManagedObjectStoreAssociatedKey); +} + +- (void)setManagedObjectStore:(RKManagedObjectStore *)managedObjectStore +{ + objc_setAssociatedObject(self, &NSManagedObject_RKManagedObjectStoreAssociatedKey, managedObjectStore, OBJC_ASSOCIATION_ASSIGN); +} + +@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 new file mode 100644 index 0000000000..d68ab6a04b --- /dev/null +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.h @@ -0,0 +1,19 @@ +// +// RKFetchRequestManagedObjectCache.h +// RestKit +// +// Created by Jeff Arena on 1/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKManagedObjectCaching.h" + +/** + Provides a simple managed object cache strategy in which every request for an object + is satisfied by dispatching an NSFetchRequest against the Core Data persistent store. + 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 + +@end diff --git a/Code/CoreData/RKFetchRequestManagedObjectCache.m b/Code/CoreData/RKFetchRequestManagedObjectCache.m new file mode 100644 index 0000000000..3409a03968 --- /dev/null +++ b/Code/CoreData/RKFetchRequestManagedObjectCache.m @@ -0,0 +1,65 @@ +// +// RKFetchRequestMappingCache.m +// RestKit +// +// Created by Jeff Arena on 1/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKFetchRequestManagedObjectCache.h" +#import "NSManagedObject+ActiveRecord.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKLog.h" +#import "RKObjectPropertyInspector.h" +#import "RKObjectPropertyInspector+CoreData.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + +@implementation RKFetchRequestManagedObjectCache + +- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity + withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute + value:(id)primaryKeyValue + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSAssert(entity, @"Cannot find existing managed object without a target class"); + NSAssert(primaryKeyAttribute, @"Cannot find existing managed object instance without mapping that defines a primaryKeyAttribute"); + NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value"); + NSAssert(managedObjectContext, @"Cannot find existing managed object with a context"); + + 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) { + object = [objects objectAtIndex:0]; + } + return object; +} + +@end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.h b/Code/CoreData/RKInMemoryManagedObjectCache.h new file mode 100644 index 0000000000..4ae4642b36 --- /dev/null +++ b/Code/CoreData/RKInMemoryManagedObjectCache.h @@ -0,0 +1,18 @@ +// +// RKInMemoryManagedObjectCache.h +// RestKit +// +// Created by Jeff Arena on 1/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#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 + +@end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.m b/Code/CoreData/RKInMemoryManagedObjectCache.m new file mode 100644 index 0000000000..01d5932629 --- /dev/null +++ b/Code/CoreData/RKInMemoryManagedObjectCache.m @@ -0,0 +1,77 @@ +// +// RKInMemoryManagedObjectCache.m +// RestKit +// +// Created by Jeff Arena on 1/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKInMemoryManagedObjectCache.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityCache.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + +static NSString * const RKInMemoryObjectManagedObjectCacheThreadDictionaryKey = @"RKInMemoryObjectManagedObjectCacheThreadDictionaryKey"; + +@implementation RKInMemoryManagedObjectCache + +- (RKEntityCache *)cacheForEntity:(NSEntityDescription *)entity inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSAssert(entity, @"Cannot find existing managed object without a target class"); + NSAssert(managedObjectContext, @"Cannot find existing managed object with a context"); + NSMutableDictionary *contextDictionary = [[[NSThread currentThread] threadDictionary] objectForKey:RKInMemoryObjectManagedObjectCacheThreadDictionaryKey]; + if (! contextDictionary) { + contextDictionary = [NSMutableDictionary dictionaryWithCapacity:1]; + [[[NSThread currentThread] threadDictionary] setObject:contextDictionary forKey:RKInMemoryObjectManagedObjectCacheThreadDictionaryKey]; + } + NSNumber *hashNumber = [NSNumber numberWithUnsignedInteger:[managedObjectContext hash]]; + 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 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/RKManagedObjectCache.h b/Code/CoreData/RKManagedObjectCache.h deleted file mode 100644 index 33d026a35c..0000000000 --- a/Code/CoreData/RKManagedObjectCache.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * RKManagedObjectCache.h - * RestKit - * - * Created by Jeff Arena on 10/15/10. - * Copyright 2010 GateGuru. All rights reserved. - * - */ - -/** - * Class used for determining the set of NSFetchRequest objects that - * map to a given request URL. - */ -@protocol RKManagedObjectCache -@required - -/** - * Must return an array containing NSFetchRequests for use in retrieving locally - * cached objects associated with a given request resourcePath. - */ -- (NSArray*)fetchRequestsForResourcePath:(NSString*)resourcePath; - -@optional - -/** - * When the managed object cache is compared to objects from a resource path - * payload, objects that are in the cache and not returned by the resource - * path are normally deleted. By returning NO from this method you can prevent - * the deletion of a given object. - */ -- (BOOL)shouldDeleteOrphanedObject:(NSManagedObject*)managedObject; - -@end diff --git a/Code/CoreData/RKManagedObjectCaching.h b/Code/CoreData/RKManagedObjectCaching.h new file mode 100644 index 0000000000..64fe4a5096 --- /dev/null +++ b/Code/CoreData/RKManagedObjectCaching.h @@ -0,0 +1,60 @@ +// +// RKManagedObjectCaching.h +// RestKit +// +// Created by Jeff Arena on 1/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +/** + 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 RKManagedObjectCaching + +@required + +/** + Retrieves a model object from the object store given a Core Data entity and + the primary key attribute and value for the desired object. + + @param entity The Core Data entity for the type of object to be retrieved from the cache. + @param primaryKeyAttribute The name of the attribute that acts as the primary key for the entity. + @param primaryKeyValue The value for the primary key attribute of the object to be retrieved from the cache. + @param mmanagedObjectContext The managed object context to be searched for a matching instance. + @return A managed object that is an instance of the given entity with a primary key and value matching + the specified parameters, or nil if no object was found. + */ +- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity + withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute + 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 672cfe8c1f..5a8d46e290 100644 --- a/Code/CoreData/RKManagedObjectLoader.h +++ b/Code/CoreData/RKManagedObjectLoader.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 2/13/11. -// Copyright 2011 Two Toasters -// +// 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,11 +28,20 @@ concerns imposed by Core Data. */ @interface RKManagedObjectLoader : RKObjectLoader { - NSManagedObjectID* _targetObjectID; + RKManagedObjectStore *_objectStore; + NSManagedObjectID* _targetObjectID; NSMutableSet* _managedObjectKeyPaths; BOOL _deleteObjectOnFailure; } -@property (nonatomic, readonly) RKManagedObjectStore* objectStore; +/** + A reference to a RestKit managed object store for interacting with Core Data + + @see RKManagedObjectStore + */ +@property (nonatomic, retain) RKManagedObjectStore* objectStore; + ++ (id)loaderWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider objectStore:(RKManagedObjectStore *)objectStore; +- (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider objectStore:(RKManagedObjectStore *)objectStore; @end diff --git a/Code/CoreData/RKManagedObjectLoader.m b/Code/CoreData/RKManagedObjectLoader.m index e07384c359..bc65db0752 100644 --- a/Code/CoreData/RKManagedObjectLoader.m +++ b/Code/CoreData/RKManagedObjectLoader.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 2/13/11. -// Copyright 2011 Two Toasters -// +// 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,37 +26,55 @@ #import "NSManagedObject+ActiveRecord.h" #import "RKObjectLoader_Internals.h" #import "RKRequest_Internals.h" +#import "RKObjectMappingProvider+CoreData.h" #import "RKLog.h" +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + @implementation RKManagedObjectLoader -- (id)init { - self = [super init]; +@synthesize objectStore = _objectStore; + ++ (id)loaderWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider objectStore:(RKManagedObjectStore *)objectStore { + return [[[self alloc] initWithURL:URL mappingProvider:mappingProvider objectStore:objectStore] autorelease]; +} + +- (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider objectStore:(RKManagedObjectStore *)objectStore { + self = [self initWithURL:URL mappingProvider:mappingProvider]; + if (self) { + _objectStore = [objectStore retain]; + } + + return self; +} + +- (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider { + self = [super initWithURL:URL mappingProvider:mappingProvider]; 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; } -- (RKManagedObjectStore*)objectStore { - return self.objectManager.objectStore; -} - #pragma mark - RKObjectMapperDelegate methods - (void)objectMapper:(RKObjectMapper*)objectMapper didMapFromObject:(id)sourceObject toObject:(id)destinationObject atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMapping*)objectMapping { @@ -70,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; @@ -87,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 @@ -95,43 +113,40 @@ - (BOOL)prepareURLRequest { // set before the managed object store. if (self.targetObject && [self.targetObject isKindOfClass:[NSManagedObject class]]) { _deleteObjectOnFailure = [(NSManagedObject*)self.targetObject isNew]; - [self.objectStore save]; + [self.objectStore save:nil]; _targetObjectID = [[(NSManagedObject*)self.targetObject objectID] retain]; } - + return [super prepareURLRequest]; } +- (NSArray *)cachedObjects { + NSFetchRequest *fetchRequest = [self.mappingProvider fetchRequestForResourcePath:self.resourcePath]; + if (fetchRequest) { + return [NSManagedObject objectsWithFetchRequest:fetchRequest]; + } + + return nil; +} + - (void)deleteCachedObjectsMissingFromResult:(RKObjectMappingResult*)result { if (! [self isGET]) { RKLogDebug(@"Skipping cleanup of objects via managed object cache: only used for GET requests."); return; } - + if ([self.URL isKindOfClass:[RKURL class]]) { - RKURL* rkURL = (RKURL*)self.URL; - - NSArray* results = [result asCollection]; - NSArray* cachedObjects = [self.objectStore objectsForResourcePath:rkURL.resourcePath]; - NSObject* managedObjectCache = self.objectStore.managedObjectCache; - BOOL queryForDeletion = [managedObjectCache respondsToSelector:@selector(shouldDeleteOrphanedObject:)]; - + NSArray *results = [result asCollection]; + NSArray *cachedObjects = [self cachedObjects]; for (id object in cachedObjects) { if (NO == [results containsObject:object]) { - if (queryForDeletion && [managedObjectCache shouldDeleteOrphanedObject:object] == NO) - { - RKLogTrace(@"Sparing orphaned object %@ even though not returned in result set", object); - } - else - { RKLogTrace(@"Deleting orphaned object %@: not found in result set and expected at this resource path", object); - [[self.objectStore managedObjectContext] deleteObject:object]; - } + [[self.objectStore managedObjectContextForCurrentThread] deleteObject:object]; } } } 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 @@ -140,26 +155,30 @@ - (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 managedObjectContext] deleteObject:backgroundThreadObject]; + [[self.objectStore managedObjectContextForCurrentThread] deleteObject:backgroundThreadObject]; } - + // If the response was successful, save the store... if ([self.response isSuccessful]) { [self deleteCachedObjectsMissingFromResult:result]; - NSError* error = [self.objectStore save]; - if (error) { + NSError *error = nil; + BOOL success = [self.objectStore save:&error]; + if (! success) { RKLogError(@"Failed to save managed object context after mapping completed: %@", [error localizedDescription]); - NSMethodSignature* signature = [self.delegate methodSignatureForSelector:@selector(objectLoader:didFailWithError:)]; + NSMethodSignature* signature = [(NSObject *)self methodSignatureForSelector:@selector(informDelegateOfError:)]; RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; - [invocation setTarget:self.delegate]; - [invocation setSelector:@selector(objectLoader:didFailWithError:)]; - [invocation setArgument:&self atIndex:2]; - [invocation setArgument:&error atIndex:3]; + [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]; @@ -174,14 +193,14 @@ - (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); NSManagedObject* objectToDelete = [self.objectStore objectWithID:_targetObjectID]; if (objectToDelete) { - [[self.objectStore managedObjectContext] deleteObject:objectToDelete]; - [self.objectStore save]; + [[self.objectStore managedObjectContextForCurrentThread] deleteObject:objectToDelete]; + [self.objectStore save:nil]; } else { RKLogWarning(@"Unable to delete existing managed object with ID: %@. Object not found in the store.", _targetObjectID); } @@ -191,4 +210,17 @@ - (void)handleResponseError { } } +- (BOOL)isResponseMappable { + if ([self.response wasLoadedFromCache]) { + NSArray* cachedObjects = [self cachedObjects]; + if (! cachedObjects) { + RKLogDebug(@"Skipping managed object mapping optimization -> Managed object cache returned nil cachedObjects for resourcePath: %@", self.resourcePath); + return [super isResponseMappable]; + } + [self informDelegateOfObjectLoadWithResultDictionary:[NSDictionary dictionaryWithObject:cachedObjects forKey:@""]]; + return NO; + } + return [super isResponseMappable]; +} + @end diff --git a/Code/CoreData/RKManagedObjectMapping.h b/Code/CoreData/RKManagedObjectMapping.h index 1c0bd97732..876ab23c9f 100644 --- a/Code/CoreData/RKManagedObjectMapping.h +++ b/Code/CoreData/RKManagedObjectMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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,74 +20,144 @@ #import #import "RKObjectMapping.h" +//#import "RKManagedObjectStore.h" +@class RKManagedObjectStore; + +/** + An RKManagedObjectMapping defines an object mapping with a Core Data destination + entity. + */ @interface RKManagedObjectMapping : RKObjectMapping { - NSEntityDescription* _entity; - NSString* _primaryKeyAttribute; - NSMutableDictionary* _relationshipToPrimaryKeyMappings; + NSEntityDescription *_entity; + NSString *_primaryKeyAttribute; + NSMutableDictionary *_relationshipToPrimaryKeyMappings; } +/** + Creates a new object mapping targetting the Core Data entity represented by objectClass + */ ++ (id)mappingForClass:(Class)objectClass inManagedObjectStore:(RKManagedObjectStore *)objectStore; + /** Creates a new object mapping targetting the specified Core Data entity */ -+ (RKManagedObjectMapping*)mappingForEntity:(NSEntityDescription*)entity; ++ (RKManagedObjectMapping *)mappingForEntity:(NSEntityDescription *)entity inManagedObjectStore:(RKManagedObjectStore *)objectStore; /** Creates a new object mapping targetting the Core Data entity with the specified name. - The entity description is fetched from the current managed object context + The entity description is fetched from the managed object context associated with objectStore */ -+ (RKManagedObjectMapping*)mappingForEntityWithName:(NSString*)entityName; ++ (RKManagedObjectMapping *)mappingForEntityWithName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)objectStore; /** The Core Data entity description used for this object mapping */ -@property (nonatomic, readonly) NSEntityDescription* entity; +@property (nonatomic, readonly) NSEntityDescription *entity; /** - The attribute containing the primary key value for the class. This is consulted by - RestKit to uniquely identify objects within the store using the primary key in your - remote backend system. + The name of the attribute on the destination entity that acts as the primary key for instances + of the entity in the remote backend system. Used to uniquely identify objects within the store + so that existing objects are updated rather than creating new ones. + + @warning Note that primaryKeyAttribute defaults to the primaryKeyAttribute configured + on the NSEntityDescription for the entity targetted by the receiving mapping. This provides + flexibility in cases where a single entity is the target of many mappings with differing + primary key definitions. + + If the primaryKeyAttribute is set on an RKManagedObjectMapping that targets an entity with a + nil primaryKeyAttribute, then the primaryKeyAttribute will be set on the entity as well for + convenience and backwards compatibility. This may change in the future. + + @see [NSEntityDescription primaryKeyAttribute] */ -@property (nonatomic, retain) NSString* primaryKeyAttribute; +@property (nonatomic, retain) NSString *primaryKeyAttribute; /** Returns a dictionary containing Core Data relationships and attribute pairs containing - the primary key for + the primary key for + */ +@property (nonatomic, readonly) NSDictionary *relationshipsAndPrimaryKeyAttributes; + +/** + The RKManagedObjectStore containing the Core Data entity being mapped */ -@property (nonatomic, readonly) NSDictionary* relationshipsAndPrimaryKeyAttributes; +@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. */ -- (void)connectRelationship:(NSString*)relationshipName withObjectForPrimaryKeyAttribute:(NSString*)primaryKeyAttribute; +- (void)connectRelationship:(NSString *)relationshipName withObjectForPrimaryKeyAttribute:(NSString *)primaryKeyAttribute; /** 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; +- (void)connectRelationshipsWithObjectsForPrimaryKeyAttributes:(NSString *)firstRelationshipName, ... NS_REQUIRES_NIL_TERMINATION; -- (id)initWithEntity:(NSEntityDescription*)entity; +/** + Conditionally connect a relationship of the object being mapped when the object being mapped has + keyPath equal to a specified value. + + For example, given a Project object associated with a User, where the 'admin' relationship is + specified by a adminID property on the managed object: + + [mapping connectRelationship:@"admin" withObjectForPrimaryKeyAttribute:@"adminID" whenValueOfKeyPath:@"userType" isEqualTo:@"Admin"]; + + Will hydrate the 'admin' 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. Note that this connection will only occur when the Product's 'userType' + property equals 'Admin'. In cases where no match occurs, the relationship connection is skipped. + + @see connectRelationship:withObjectForPrimaryKeyAttribute: + */ +- (void)connectRelationship:(NSString *)relationshipName withObjectForPrimaryKeyAttribute:(NSString *)primaryKeyAttribute whenValueOfKeyPath:(NSString *)keyPath isEqualTo:(id)value; + +/** + Conditionally connect a relationship of the object being mapped when the object being mapped has + block evaluate to YES. This variant is useful in cases where you want to execute an arbitrary + block to determine whether or not to connect a relationship. + + For example, given a Project object associated with a User, where the 'admin' relationship is + specified by a adminID property on the managed object: + + [mapping connectRelationship:@"admin" withObjectForPrimaryKeyAttribute:@"adminID" usingEvaluationBlock:^(id data) { + return [User isAuthenticated]; + }]; + + Will hydrate the 'admin' 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. Note that this connection will only occur when the provided block evalutes to YES. + In cases where no match occurs, the relationship connection is skipped. + + @see connectRelationship:withObjectForPrimaryKeyAttribute: + */ +- (void)connectRelationship:(NSString *)relationshipName withObjectForPrimaryKeyAttribute:(NSString *)primaryKeyAttribute usingEvaluationBlock:(BOOL (^)(id data))block; + +/** + Initialize a managed object mapping with a Core Data entity description and a RestKit managed object store + */ +- (id)initWithEntity:(NSEntityDescription *)entity inManagedObjectStore:(RKManagedObjectStore *)objectStore; /** Returns the default value for the specified attribute as expressed in the Core Data entity definition. This value will be assigned if the object mapping is applied and a value for a missing attribute is not present in the payload. */ -- (id)defaultValueForMissingAttribute:(NSString*)attributeName; +- (id)defaultValueForMissingAttribute:(NSString *)attributeName; @end diff --git a/Code/CoreData/RKManagedObjectMapping.m b/Code/CoreData/RKManagedObjectMapping.m index fddb878dd9..8008218f90 100644 --- a/Code/CoreData/RKManagedObjectMapping.m +++ b/Code/CoreData/RKManagedObjectMapping.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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,9 +20,10 @@ #import "RKManagedObjectMapping.h" #import "NSManagedObject+ActiveRecord.h" -#import "RKObjectManager.h" #import "RKManagedObjectStore.h" +#import "RKDynamicObjectMappingMatcher.h" #import "RKObjectPropertyInspector+CoreData.h" +#import "NSEntityDescription+RKAdditions.h" #import "RKLog.h" // Set Logging Component @@ -33,27 +34,42 @@ @implementation RKManagedObjectMapping @synthesize entity = _entity; @synthesize primaryKeyAttribute = _primaryKeyAttribute; +@synthesize objectStore = _objectStore; + (id)mappingForClass:(Class)objectClass { - return [self mappingForEntityWithName:NSStringFromClass(objectClass)]; + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must provide a managedObjectStore. Invoke mappingForClass:inManagedObjectStore: instead."] + userInfo:nil]; +} + ++ (id)mappingForClass:(Class)objectClass inManagedObjectStore:(RKManagedObjectStore *)objectStore { + return [self mappingForEntityWithName:NSStringFromClass(objectClass) inManagedObjectStore:objectStore]; } -+ (RKManagedObjectMapping*)mappingForEntity:(NSEntityDescription*)entity { - return [[[self alloc] initWithEntity:entity] autorelease]; ++ (RKManagedObjectMapping *)mappingForEntity:(NSEntityDescription*)entity inManagedObjectStore:(RKManagedObjectStore *)objectStore { + return [[[self alloc] initWithEntity:entity inManagedObjectStore:objectStore] autorelease]; } -+ (RKManagedObjectMapping*)mappingForEntityWithName:(NSString*)entityName { - return [self mappingForEntity:[NSEntityDescription entityForName:entityName inManagedObjectContext:[NSManagedObject managedObjectContext]]]; ++ (RKManagedObjectMapping *)mappingForEntityWithName:(NSString*)entityName inManagedObjectStore:(RKManagedObjectStore *)objectStore { + return [self mappingForEntity:[NSEntityDescription entityForName:entityName inManagedObjectContext:objectStore.primaryManagedObjectContext] + inManagedObjectStore:objectStore]; } -- (id)initWithEntity:(NSEntityDescription*)entity { +- (id)initWithEntity:(NSEntityDescription*)entity inManagedObjectStore:(RKManagedObjectStore*)objectStore { NSAssert(entity, @"Cannot initialize an RKManagedObjectMapping without an entity. Maybe you want RKObjectMapping instead?"); + NSAssert(objectStore, @"Object store cannot be nil"); + Class objectClass = NSClassFromString([entity managedObjectClassName]); + NSAssert(objectClass, @"The managedObjectClass for an object mapped entity cannot be nil."); self = [self init]; if (self) { - self.objectClass = NSClassFromString([entity managedObjectClassName]); + _objectClass = [objectClass retain]; _entity = [entity retain]; + _objectStore = objectStore; + + [self addObserver:self forKeyPath:@"entity" options:NSKeyValueObservingOptionInitial context:nil]; + [self addObserver:self forKeyPath:@"primaryKeyAttribute" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil]; } - + return self; } @@ -62,11 +78,14 @@ - (id)init { if (self) { _relationshipToPrimaryKeyMappings = [[NSMutableDictionary alloc] init]; } - + return self; } - (void)dealloc { + [self removeObserver:self forKeyPath:@"entity"]; + [self removeObserver:self forKeyPath:@"primaryKeyAttribute"]; + [_entity release]; [_relationshipToPrimaryKeyMappings release]; [super dealloc]; @@ -85,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... @@ -93,26 +112,35 @@ - (void)connectRelationshipsWithObjectsForPrimaryKeyAttributes:(NSString*)firstR va_end(args); } +- (void)connectRelationship:(NSString*)relationshipName withObjectForPrimaryKeyAttribute:(NSString*)primaryKeyAttribute whenValueOfKeyPath:(NSString*)keyPath isEqualTo:(id)value { + NSAssert([_relationshipToPrimaryKeyMappings objectForKey:relationshipName] == nil, @"Cannot add connect relationship %@ by primary key, a mapping already exists.", relationshipName); + RKDynamicObjectMappingMatcher* matcher = [[RKDynamicObjectMappingMatcher alloc] initWithKey:keyPath value:value primaryKeyAttribute:primaryKeyAttribute]; + [_relationshipToPrimaryKeyMappings setObject:matcher forKey:relationshipName]; + [matcher release]; +} + +- (void)connectRelationship:(NSString*)relationshipName withObjectForPrimaryKeyAttribute:(NSString*)primaryKeyAttribute usingEvaluationBlock:(BOOL (^)(id data))block { + NSAssert([_relationshipToPrimaryKeyMappings objectForKey:relationshipName] == nil, @"Cannot add connect relationship %@ by primary key, a mapping already exists.", relationshipName); + RKDynamicObjectMappingMatcher* matcher = [[RKDynamicObjectMappingMatcher alloc] initWithPrimaryKeyAttribute:primaryKeyAttribute evaluationBlock:block]; + [_relationshipToPrimaryKeyMappings setObject:matcher forKey:relationshipName]; + [matcher release]; +} + - (id)defaultValueForMissingAttribute:(NSString*)attributeName { NSAttributeDescription *desc = [[self.entity attributesByName] valueForKey:attributeName]; return [desc defaultValue]; } -- (id)mappableObjectForData:(id)mappableData { +- (id)mappableObjectForData:(id)mappableData { NSAssert(mappableData, @"Mappable data cannot be nil"); - - // TODO: We do not want to be using this singleton reference to the object store. - // Clean this up when we update the Core Data internals - RKManagedObjectStore* objectStore = [RKObjectManager sharedManager].objectStore; - NSAssert(objectStore, @"Object store 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 @@ -123,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]; @@ -132,19 +160,36 @@ - (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) { - object = [objectStore findOrCreateInstanceOfEntity:entity withPrimaryKeyAttribute:primaryKeyAttribute andValue:primaryKeyValue]; - NSAssert2(object, @"Failed creation of managed object with entity '%@' and primary key value '%@'", entity.name, primaryKeyValue); - } else { + if (primaryKeyAttribute && primaryKeyValue && NO == [primaryKeyValue isEqual:[NSNull null]]) { + object = [self.objectStore.cacheStrategy findInstanceOfEntity:entity + 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.managedObjectContext] autorelease]; + 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; } @@ -153,8 +198,23 @@ - (Class)classForProperty:(NSString*)propertyName { if (! propertyClass) { propertyClass = [[RKObjectPropertyInspector sharedInspector] typeForProperty:propertyName ofEntity:self.entity]; } - + return propertyClass; } +/* + 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 primaryKeyAttributeName]; + } + } else if ([keyPath isEqualToString:@"primaryKeyAttribute"]) { + if (! self.entity.primaryKeyAttribute) { + self.entity.primaryKeyAttributeName = self.primaryKeyAttribute; + } + } +} @end diff --git a/Code/CoreData/RKManagedObjectMappingOperation.h b/Code/CoreData/RKManagedObjectMappingOperation.h index 056a30df1d..14391d582a 100644 --- a/Code/CoreData/RKManagedObjectMappingOperation.h +++ b/Code/CoreData/RKManagedObjectMappingOperation.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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 c6bf89c7bc..4d52293e6b 100644 --- a/Code/CoreData/RKManagedObjectMappingOperation.m +++ b/Code/CoreData/RKManagedObjectMappingOperation.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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,6 +21,9 @@ #import "RKManagedObjectMappingOperation.h" #import "RKManagedObjectMapping.h" #import "NSManagedObject+ActiveRecord.h" +#import "RKDynamicObjectMappingMatcher.h" +#import "RKManagedObjectCaching.h" +#import "RKManagedObjectStore.h" #import "RKLog.h" // Set Logging Component @@ -29,67 +32,104 @@ @implementation RKManagedObjectMappingOperation -// TODO: Move this to a better home to take exposure out of the mapper -- (Class)operationClassForMapping:(RKObjectMapping *)mapping { - Class managedMappingClass = NSClassFromString(@"RKManagedObjectMapping"); - Class managedMappingOperationClass = NSClassFromString(@"RKManagedObjectMappingOperation"); - if (managedMappingClass != nil && [mapping isMemberOfClass:managedMappingClass]) { - return managedMappingOperationClass; - } - - return [RKObjectMappingOperation class]; -} - - (void)connectRelationship:(NSString *)relationshipName { NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping*)self.objectMapping relationshipsAndPrimaryKeyAttributes]; - NSString* primaryKeyAttribute = [relationshipsAndPrimaryKeyAttributes objectForKey:relationshipName]; + id primaryKeyObject = [relationshipsAndPrimaryKeyAttributes objectForKey:relationshipName]; + NSString* primaryKeyAttribute = nil; + if ([primaryKeyObject isKindOfClass:[RKDynamicObjectMappingMatcher class]]) { + RKLogTrace(@"Found a dynamic matcher attempting to connect relationshipName: %@", relationshipName); + RKDynamicObjectMappingMatcher* matcher = (RKDynamicObjectMappingMatcher*)primaryKeyObject; + if ([matcher isMatchForData:self.destinationObject]) { + primaryKeyAttribute = matcher.primaryKeyAttribute; + RKLogTrace(@"Dynamic matched succeeded. Proceeding to connect relationshipName '%@' using primaryKeyAttribute '%@'", relationshipName, primaryKeyAttribute); + } else { + RKLogTrace(@"Dynamic matcher match failed. Skipping connection of relationshipName: %@", relationshipName); + return; + } + } else if ([primaryKeyObject isKindOfClass:[NSString class]]) { + primaryKeyAttribute = (NSString*)primaryKeyObject; + } + NSAssert(primaryKeyAttribute, @"Cannot connect relationship without primaryKeyAttribute"); + RKObjectRelationshipMapping* relationshipMapping = [self.objectMapping mappingForRelationship:relationshipName]; - id mapping = relationshipMapping.mapping; + RKObjectMappingDefinition *mapping = relationshipMapping.mapping; NSAssert(mapping, @"Attempted to connect relationship for keyPath '%@' without a relationship mapping defined."); if (! [mapping isKindOfClass:[RKObjectMapping class]]) { RKLogWarning(@"Can only connect relationships for RKObjectMapping relationships. Found %@: Skipping...", NSStringFromClass([mapping class])); return; } - RKObjectMapping* objectMapping = (RKObjectMapping*)mapping; + RKManagedObjectMapping *objectMapping = (RKManagedObjectMapping *) mapping; NSAssert(relationshipMapping, @"Unable to find relationship mapping '%@' to connect by primaryKey", relationshipName); NSAssert([relationshipMapping isKindOfClass:[RKObjectRelationshipMapping class]], @"Expected mapping for %@ to be a relationship mapping", relationshipName); NSAssert([relationshipMapping.mapping isKindOfClass:[RKManagedObjectMapping class]], @"Can only connect RKManagedObjectMapping relationships"); NSString* primaryKeyAttributeOfRelatedObject = [(RKManagedObjectMapping*)objectMapping primaryKeyAttribute]; NSAssert(primaryKeyAttributeOfRelatedObject, @"Cannot connect relationship: mapping for %@ has no primary key attribute specified", NSStringFromClass(objectMapping.objectClass)); id valueOfLocalPrimaryKeyAttribute = [self.destinationObject valueForKey:primaryKeyAttribute]; - RKLogDebug(@"Connecting relationship at keyPath '%@' to object with primaryKey attribute '%@'", relationshipName, primaryKeyAttributeOfRelatedObject); if (valueOfLocalPrimaryKeyAttribute) { - id relatedObject = [objectMapping.objectClass findFirstByAttribute:primaryKeyAttributeOfRelatedObject withValue:valueOfLocalPrimaryKeyAttribute]; - if (relatedObject) { - RKLogTrace(@"Connected relationship '%@' to object with primary key value '%@': %@", relationshipName, valueOfLocalPrimaryKeyAttribute, relatedObject); + id relatedObject = nil; + if ([valueOfLocalPrimaryKeyAttribute conformsToProtocol:@protocol(NSFastEnumeration)]) { + RKLogTrace(@"Connecting has-many relationship at keyPath '%@' to object with primaryKey attribute '%@'", relationshipName, primaryKeyAttributeOfRelatedObject); + + // 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(@"Failed to find object to connect relationship '%@' with primary key value '%@'", relationshipName, valueOfLocalPrimaryKeyAttribute); + RKLogTrace(@"Connecting has-one relationship at keyPath '%@' to object with primaryKey attribute '%@'", relationshipName, primaryKeyAttributeOfRelatedObject); + + // 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) { + 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); + } + if ([relatedObject isKindOfClass:[NSManagedObject class]]) { + // Sanity check the managed object contexts + NSAssert([[(NSManagedObject *)self.destinationObject managedObjectContext] isEqual:[(NSManagedObject *)relatedObject managedObjectContext]], nil); } - [self.destinationObject setValue:relatedObject forKey:relationshipName]; + RKLogTrace(@"setValue of %@ forKeyPath %@", relatedObject, relationshipName); + [self.destinationObject setValue:relatedObject forKeyPath:relationshipName]; } else { RKLogTrace(@"Failed to find primary key value for attribute '%@'", primaryKeyAttribute); } } - (void)connectRelationships { - if ([self.objectMapping isKindOfClass:[RKManagedObjectMapping class]]) { - NSDictionary* relationshipsAndPrimaryKeyAttributes = [(RKManagedObjectMapping*)self.objectMapping relationshipsAndPrimaryKeyAttributes]; - for (NSString* relationshipName in relationshipsAndPrimaryKeyAttributes) { - if (self.queue) { - RKLogTrace(@"Enqueueing relationship connection using operation queue"); - [self.queue addOperationWithBlock:^{ - [self 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 new file mode 100644 index 0000000000..64abcf570e --- /dev/null +++ b/Code/CoreData/RKManagedObjectSearchEngine.h @@ -0,0 +1,51 @@ +// +// RKManagedObjectSearchEngine.h +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKSearchEngine.h" + +@interface RKManagedObjectSearchEngine : NSObject { + RKSearchMode _mode; +} + +/** + * The type of searching to perform. Can be either RKSearchModeAnd or RKSearchModeOr. + * + * Defaults to RKSearchModeOr + */ +@property (nonatomic, assign) RKSearchMode mode; + +/** + * Construct a new search engine + */ ++ (id)searchEngine; + +/** + * Normalize and tokenize the provided string into an NSArray. + * Note that returned value may contain entries of empty strings. + */ ++ (NSArray*)tokenizedNormalizedString:(NSString*)string; + +/** + * Generate a predicate for the supplied search term against + * searchableAttributes (defined for an RKSearchableManagedObject) + */ +- (NSPredicate*)predicateForSearch:(NSString*)searchText; + +@end diff --git a/Code/CoreData/RKManagedObjectSearchEngine.m b/Code/CoreData/RKManagedObjectSearchEngine.m new file mode 100644 index 0000000000..0740d99bc7 --- /dev/null +++ b/Code/CoreData/RKManagedObjectSearchEngine.m @@ -0,0 +1,100 @@ +// +// RKManagedObjectSearchEngine.m +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKManagedObjectSearchEngine.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + + +@implementation RKManagedObjectSearchEngine + +static NSMutableCharacterSet* __removeSet; + +@synthesize mode = _mode; + ++ (id)searchEngine { + RKManagedObjectSearchEngine* searchEngine = [[[RKManagedObjectSearchEngine alloc] init] autorelease]; + return searchEngine; +} + +- (id)init { + if (self = [super init]) { + _mode = RKSearchModeOr; + } + + 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]; +} + +#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; +} + +- (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; + } +} + +@end diff --git a/Code/CoreData/RKManagedObjectSeeder.h b/Code/CoreData/RKManagedObjectSeeder.h index ae5798f609..c856a81ce6 100644 --- a/Code/CoreData/RKManagedObjectSeeder.h +++ b/Code/CoreData/RKManagedObjectSeeder.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 3/4/10. -// Copyright 2010 Two Toasters -// +// 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 785144358b..f779984f41 100644 --- a/Code/CoreData/RKManagedObjectSeeder.m +++ b/Code/CoreData/RKManagedObjectSeeder.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 3/4/10. -// Copyright 2010 Two Toasters -// +// 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,45 +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 = [[_manager objectStore] save]; - if (error != nil) { - 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.", + 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.", destinationPath, basePath, storeFileName); - - exit(1); + + exit(1); } @end diff --git a/Code/CoreData/RKManagedObjectStore.h b/Code/CoreData/RKManagedObjectStore.h index 7a2077579e..75bfd8e64f 100644 --- a/Code/CoreData/RKManagedObjectStore.h +++ b/Code/CoreData/RKManagedObjectStore.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/22/09. -// Copyright 2009 Two Toasters -// +// 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,7 +19,8 @@ // #import -#import "RKManagedObjectCache.h" +#import "RKManagedObjectMapping.h" +#import "RKManagedObjectCaching.h" @class RKManagedObjectStore; @@ -46,12 +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; - NSObject* _managedObjectCache; + NSPersistentStoreCoordinator* _persistentStoreCoordinator; } // The delegate for this object store @@ -67,18 +67,40 @@ extern NSString* const RKManagedObjectStoreDidFailSaveNotification; @property (nonatomic, readonly) NSManagedObjectModel* managedObjectModel; @property (nonatomic, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator; +///----------------------------------------------------------------------------- +/// @name Accessing the Default Object Store +///----------------------------------------------------------------------------- + ++ (RKManagedObjectStore *)defaultObjectStore; ++ (void)setDefaultObjectStore:(RKManagedObjectStore *)objectStore; + +///----------------------------------------------------------------------------- +/// @name Deleting Store Files +///----------------------------------------------------------------------------- + /** - * Managed object cache provides support for automatic removal of objects pruned - * from a server side load. Also used to provide offline object loading + Deletes the SQLite file backing an RKManagedObjectStore instance at a given path. + + @param path The complete path to the store file to delete. */ -@property (nonatomic, retain) NSObject* managedObjectCache; ++ (void)deleteStoreAtPath:(NSString *)path; -/* - * This returns an appropriate managed object context for this object store. - * Because of the intrecacies of how Core Data works across threads it returns - * a different NSManagedObjectContext for each thread. +/** + Deletes the SQLite file backing an RKManagedObjectStore instance with a given + filename within the application data directory. + + @param filename The name of the file within the application data directory backing a managed object store. */ -@property (nonatomic, readonly) NSManagedObjectContext* managedObjectContext; ++ (void)deleteStoreInApplicationDataDirectoryWithFilename:(NSString *)filename; + +///----------------------------------------------------------------------------- +/// @name Initializing an Object Store +///----------------------------------------------------------------------------- + +/** + + */ +@property (nonatomic, retain) NSObject *cacheStrategy; /** * Initialize a new managed object store with a SQLite database with the filename specified @@ -111,39 +133,46 @@ extern NSString* const RKManagedObjectStoreDidFailSaveNotification; /** * Save the current contents of the managed object store */ -- (NSError*)save; +- (BOOL)save:(NSError **)error; /** - * This deletes and recreates the managed object context and - * persistant store, effectively clearing all data + * This deletes and recreates the managed object context and + * 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; +///----------------------------------------------------------------------------- +/// @name Retrieving Managed Object Contexts +///----------------------------------------------------------------------------- + /** - * Retrieves a model object from the object store given a Core Data entity and - * the primary key attribute and value for the desired object. Internally, this method - * constructs a thread-local cache of managed object instances to avoid repeated fetches from the store + Retrieves the Managed Object Context for the main thread that was initialized when + the object store was created. */ -- (NSManagedObject*)findOrCreateInstanceOfEntity:(NSEntityDescription*)entity withPrimaryKeyAttribute:(NSString*)primaryKeyAttribute andValue:(id)primaryKeyValue; +@property (nonatomic, retain, readonly) NSManagedObjectContext *primaryManagedObjectContext; /** - * Returns an array of objects that the 'live' at the specified resource path. Usage of this - * method requires that you have provided an implementation of the managed object cache - * - * See managedObjectCache above + Instantiates a new managed object context + */ +- (NSManagedObjectContext *)newManagedObjectContext; + +/* + * This returns an appropriate managed object context for this object store. + * Because of the intrecacies of how Core Data works across threads it returns + * a different NSManagedObjectContext for each thread. */ -- (NSArray*)objectsForResourcePath:(NSString*)resourcePath; +- (NSManagedObjectContext *)managedObjectContextForCurrentThread; @end diff --git a/Code/CoreData/RKManagedObjectStore.m b/Code/CoreData/RKManagedObjectStore.m index 817a3f32ea..bfb196e23e 100644 --- a/Code/CoreData/RKManagedObjectStore.m +++ b/Code/CoreData/RKManagedObjectStore.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/22/09. -// Copyright 2009 Two Toasters -// +// 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,10 +19,17 @@ // #import "RKManagedObjectStore.h" -#import "RKAlert.h" #import "NSManagedObject+ActiveRecord.h" #import "RKLog.h" +#import "RKSearchWordObserver.h" +#import "RKObjectPropertyInspector.h" +#import "RKObjectPropertyInspector+CoreData.h" +#import "RKAlert.h" #import "RKDirectory.h" +#import "RKInMemoryManagedObjectCache.h" +#import "RKFetchRequestManagedObjectCache.h" +#import "NSBundle+RKAdditions.h" +#import "NSManagedObjectContext+RKAdditions.h" // Set Logging Component #undef RKLogComponent @@ -32,7 +39,11 @@ static NSString* const RKManagedObjectStoreThreadDictionaryContextKey = @"RKManagedObjectStoreThreadDictionaryContextKey"; static NSString* const RKManagedObjectStoreThreadDictionaryEntityCacheKey = @"RKManagedObjectStoreThreadDictionaryEntityCacheKey"; -@interface RKManagedObjectStore (Private) +static RKManagedObjectStore *defaultObjectStore = nil; + +@interface RKManagedObjectStore () +@property (nonatomic, retain, readwrite) NSManagedObjectContext *primaryManagedObjectContext; + - (id)initWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)nilOrDirectoryPath usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate; - (void)createPersistentStoreCoordinator; - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase; @@ -46,7 +57,39 @@ @implementation RKManagedObjectStore @synthesize pathToStoreFile = _pathToStoreFile; @synthesize managedObjectModel = _managedObjectModel; @synthesize persistentStoreCoordinator = _persistentStoreCoordinator; -@synthesize managedObjectCache = _managedObjectCache; +@synthesize cacheStrategy = _cacheStrategy; +@synthesize primaryManagedObjectContext; + ++ (RKManagedObjectStore *)defaultObjectStore { + return defaultObjectStore; +} + ++ (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; + 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); + } + } else { + RKLogWarning(@"Asked to delete persistent store but no store file exists at path: %@", storeURL.path); + } +} + ++ (void)deleteStoreInApplicationDataDirectoryWithFilename:(NSString *)filename +{ + NSString *path = [[RKDirectory applicationDataDirectory] stringByAppendingPathComponent:filename]; + [self deleteStoreAtPath:path]; +} + (RKManagedObjectStore*)objectStoreWithStoreFilename:(NSString*)storeFilename { return [self objectStoreWithStoreFilename:storeFilename usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil]; @@ -61,140 +104,190 @@ + (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]]; } - _managedObjectModel = [nilOrManagedObjectModel retain]; - + NSMutableArray* allManagedObjectModels = [NSMutableArray arrayWithObject:nilOrManagedObjectModel]; + _managedObjectModel = [[NSManagedObjectModel modelByMergingModels:allManagedObjectModels] retain]; + _delegate = delegate; + if (nilOrNameOfSeedDatabaseInMainBundle) { [self createStoreIfNecessaryUsingSeedDatabase:nilOrNameOfSeedDatabaseInMainBundle]; } - - _delegate = delegate; - - [self createPersistentStoreCoordinator]; - } - - return self; + + [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; +} + +- (void)setThreadLocalObject:(id)value forKey:(id)key { + NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; + NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self]; + if (! [threadDictionary valueForKey:objectStoreKey]) { + [threadDictionary setValue:[NSMutableDictionary dictionary] forKey:objectStoreKey]; + } + + [[threadDictionary objectForKey:objectStoreKey] setObject:value forKey:key]; +} + +- (id)threadLocalObjectForKey:(id)key { + NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; + NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self]; + if (! [threadDictionary valueForKey:objectStoreKey]) { + [threadDictionary setObject:[NSMutableDictionary dictionary] forKey:objectStoreKey]; + } + + return [[threadDictionary objectForKey:objectStoreKey] objectForKey:key]; +} + +- (void)removeThreadLocalObjectForKey:(id)key { + NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; + NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self]; + if (! [threadDictionary valueForKey:objectStoreKey]) { + [threadDictionary setObject:[NSMutableDictionary dictionary] forKey:objectStoreKey]; + } + + [[threadDictionary objectForKey:objectStoreKey] removeObjectForKey:key]; } - (void)clearThreadLocalStorage { // Clear out our Thread local information - NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; - if ([threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryContextKey]) { - [threadDictionary removeObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; + NSManagedObjectContext *managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; + if (managedObjectContext) { + [self removeThreadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; } - if ([threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]) { - [threadDictionary removeObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]; + if ([self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]) { + [self removeThreadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]; } } - (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; - [_managedObjectCache release]; - _managedObjectCache = nil; - - [super dealloc]; + _persistentStoreCoordinator = nil; + [_cacheStrategy release]; + _cacheStrategy = nil; + [primaryManagedObjectContext release]; + primaryManagedObjectContext = nil; + + [super dealloc]; } /** Performs the save action for the application, which is to send the save: message to the application's managed object context. */ -- (NSError*)save { - NSManagedObjectContext* moc = [self managedObjectContext]; - NSError *error; - @try { - if (![moc save:&error]) { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) { - [self.delegate managedObjectStore:self didFailToSaveContext:moc error:error exception:nil]; - } - - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:error forKey:@"error"]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKManagedObjectStoreDidFailSaveNotification object:self userInfo:userInfo]; - - if ([[error domain] isEqualToString:@"NSCocoaErrorDomain"]) { - NSDictionary *userInfo = [error 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"]); - } - } - return error; - } - } - @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 nil; +- (BOOL)save:(NSError **)error { + 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"]); + } + } + + 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; } -- (NSManagedObjectContext*)newManagedObjectContext { - NSManagedObjectContext* managedObjectContext = [[NSManagedObjectContext alloc] init]; - [managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator]; - [managedObjectContext setUndoManager:nil]; - [managedObjectContext setMergePolicy:NSOverwriteMergePolicy]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(objectsDidChange:) - name:NSManagedObjectContextObjectsDidChangeNotification - object:managedObjectContext]; - return managedObjectContext; +- (NSManagedObjectContext *)newManagedObjectContext { + NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init]; + [managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator]; + [managedObjectContext setUndoManager:nil]; + [managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy]; + managedObjectContext.managedObjectStore = self; + + return managedObjectContext; } - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase { @@ -202,43 +295,44 @@ - (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase { NSString* seedDatabasePath = [[NSBundle mainBundle] pathForResource:seedDatabase ofType:nil]; 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); } } - (void)createPersistentStoreCoordinator { - NSURL *storeUrl = [NSURL fileURLWithPath:self.pathToStoreFile]; - - NSError *error; + 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]; + + 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; +- (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:)]) { @@ -251,154 +345,77 @@ - (void)deletePersistantStoreUsingSeedDatabaseName:(NSString *)seedFile { } else { RKLogWarning(@"Asked to delete persistent store but no store file exists at path: %@", storeURL.path); } - - [_persistentStoreCoordinator release]; - _persistentStoreCoordinator = nil; - - [self clearThreadLocalStorage]; - - if (seedFile) { + + [_persistentStoreCoordinator release]; + _persistentStoreCoordinator = nil; + + if (seedFile) { [self createStoreIfNecessaryUsingSeedDatabase:seedFile]; } - [self createPersistentStoreCoordinator]; + [self createPersistentStoreCoordinator]; + + // Recreate the MOC + self.primaryManagedObjectContext = [[self newManagedObjectContext] autorelease]; } -- (void)deletePersistantStore { - [self deletePersistantStoreUsingSeedDatabaseName:nil]; +- (void)deletePersistentStore { + [self deletePersistentStoreUsingSeedDatabaseName:nil]; } -/** - * - * Override managedObjectContext getter to ensure we return a separate context - * for each NSThread. - * - */ --(NSManagedObjectContext*)managedObjectContext { - NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; - NSManagedObjectContext* backgroundThreadContext = [threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; - if (!backgroundThreadContext) { - backgroundThreadContext = [self newManagedObjectContext]; - [threadDictionary setObject:backgroundThreadContext forKey:RKManagedObjectStoreThreadDictionaryContextKey]; - [backgroundThreadContext release]; - - [[NSNotificationCenter defaultCenter] addObserver:self +- (NSManagedObjectContext *)managedObjectContextForCurrentThread { + if ([NSThread isMainThread]) { + return self.primaryManagedObjectContext; + } + + // Background threads leverage thread-local storage + NSManagedObjectContext* managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey]; + if (!managedObjectContext) { + managedObjectContext = [self newManagedObjectContext]; + + // Store into thread local storage dictionary + [self setThreadLocalObject:managedObjectContext forKey:RKManagedObjectStoreThreadDictionaryContextKey]; + [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:backgroundThreadContext]; - } - return backgroundThreadContext; + name:NSManagedObjectContextDidSaveNotification + object:managedObjectContext]; + } + + return managedObjectContext; } - (void)mergeChangesOnMainThreadWithNotification:(NSNotification*)notification { - assert([NSThread isMainThread]); - [self.managedObjectContext 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]; -} - -- (void)objectsDidChange:(NSNotification*)notification { - NSDictionary* userInfo = notification.userInfo; - NSSet* insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey]; - NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; - - for (NSManagedObject* object in insertedObjects) { - if ([object respondsToSelector:@selector(primaryKeyProperty)]) { - Class theClass = [object class]; - NSString* primaryKey = [theClass performSelector:@selector(primaryKeyProperty)]; - id primaryKeyValue = [object valueForKey:primaryKey]; - - NSMutableDictionary* classCache = [threadDictionary objectForKey:theClass]; - if (classCache && primaryKeyValue && [classCache objectForKey:primaryKeyValue] == nil) { - [classCache setObject:object forKey:primaryKeyValue]; - } - } - } + // Merge changes into the main context on the main thread + [self performSelectorOnMainThread:@selector(mergeChangesOnMainThreadWithNotification:) withObject:notification waitUntilDone:YES]; } #pragma mark - #pragma mark Helpers -- (NSManagedObject*)objectWithID:(NSManagedObjectID*)objectID { - return [self.managedObjectContext objectWithID:objectID]; +- (NSManagedObject*)objectWithID:(NSManagedObjectID *)objectID { + NSAssert(objectID, @"Cannot fetch a managedObject with a nil objectID"); + return [[self managedObjectContextForCurrentThread] objectWithID:objectID]; } - (NSArray*)objectsWithIDs:(NSArray*)objectIDs { - NSMutableArray* objects = [[NSMutableArray alloc] init]; - for (NSManagedObjectID* objectID in objectIDs) { - [objects addObject:[self.managedObjectContext objectWithID:objectID]]; - } - NSArray* objectArray = [NSArray arrayWithArray:objects]; - [objects release]; - - return objectArray; -} - -- (NSManagedObject*)findOrCreateInstanceOfEntity:(NSEntityDescription*)entity withPrimaryKeyAttribute:(NSString*)primaryKeyAttribute andValue:(id)primaryKeyValue { - NSAssert(entity, @"Cannot instantiate managed object without a target class"); - NSAssert(primaryKeyAttribute, @"Cannot find existing managed object instance without a primary key attribute"); - NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value"); - NSManagedObject* object = nil; - - // 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 = [primaryKeyValue respondsToSelector:@selector(stringValue)] ? [primaryKeyValue stringValue] : primaryKeyValue; - NSArray* objects = nil; - NSString* entityName = entity.name; - NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary]; - - if (nil == [threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]) { - [threadDictionary setObject:[NSMutableDictionary dictionary] forKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]; + NSMutableArray* objects = [[NSMutableArray alloc] init]; + for (NSManagedObjectID* objectID in objectIDs) { + [objects addObject:[self objectWithID:objectID]]; } - - // Construct the cache if necessary - NSMutableDictionary* entityCache = [threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]; - if (nil == [entityCache objectForKey:entityName]) { - NSFetchRequest* fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; - [fetchRequest setEntity:entity]; - [fetchRequest setReturnsObjectsAsFaults:NO]; - objects = [NSManagedObject executeFetchRequest:fetchRequest]; - RKLogInfo(@"Caching all %lu %@ objects to thread local storage", (unsigned long) [objects count], entity.name); - NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; - BOOL coerceToString = [[[objects lastObject] valueForKey:primaryKeyAttribute] respondsToSelector:@selector(stringValue)]; - for (id theObject in objects) { - id attributeValue = [theObject valueForKey:primaryKeyAttribute]; - // Coerce to a string if possible - attributeValue = coerceToString ? [attributeValue stringValue] : attributeValue; - if (attributeValue) { - [dictionary setObject:theObject forKey:attributeValue]; - } - } - - [entityCache setObject:dictionary forKey:entityName]; - } - - NSMutableDictionary* dictionary = [entityCache objectForKey:entityName]; - NSAssert1(dictionary, @"Thread local cache of %@ objects should not be nil", entityName); - object = [dictionary objectForKey:lookupValue]; - - if (object == nil) { - object = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext] autorelease]; - [dictionary setObject:object forKey:lookupValue]; - } - - return object; -} + NSArray* objectArray = [NSArray arrayWithArray:objects]; + [objects release]; -- (NSArray*)objectsForResourcePath:(NSString *)resourcePath { - NSArray* cachedObjects = nil; - - if (self.managedObjectCache) { - NSArray* cacheFetchRequests = [self.managedObjectCache fetchRequestsForResourcePath:resourcePath]; - cachedObjects = [NSManagedObject objectsWithFetchRequests:cacheFetchRequests]; - } - - return cachedObjects; + return objectArray; } @end diff --git a/Code/CoreData/RKManagedObjectThreadSafeInvocation.h b/Code/CoreData/RKManagedObjectThreadSafeInvocation.h index a64f0868b0..8d259715ab 100644 --- a/Code/CoreData/RKManagedObjectThreadSafeInvocation.h +++ b/Code/CoreData/RKManagedObjectThreadSafeInvocation.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/12/11. -// Copyright 2011 Two Toasters -// +// 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 ebc2da6bca..d4d805c5e1 100644 --- a/Code/CoreData/RKManagedObjectThreadSafeInvocation.m +++ b/Code/CoreData/RKManagedObjectThreadSafeInvocation.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/12/11. -// Copyright 2011 Two Toasters -// +// 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,40 +64,40 @@ - (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) { if ([subObject isKindOfClass:[NSManagedObjectID class]]) { + NSAssert(self.objectStore, @"Object store cannot be nil"); NSManagedObject* managedObject = [self.objectStore objectWithID:(NSManagedObjectID*)subObject]; [collection addObject:managedObject]; } else { [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]]; @@ -95,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 new file mode 100644 index 0000000000..3cb5e47421 --- /dev/null +++ b/Code/CoreData/RKObjectMappingProvider+CoreData.h @@ -0,0 +1,54 @@ +// +// RKObjectMappingProvider+CoreData.h +// RestKit +// +// Created by Jeff Arena on 1/26/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingProvider.h" +#import + +typedef NSFetchRequest *(^RKObjectMappingProviderFetchRequestBlock)(NSString *resourcePath); + +/** + Provides extensions to RKObjectMappingProvider to support Core Data specific + functionality. + */ +@interface RKObjectMappingProvider (CoreData) + +/** + 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 + 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 + to determine if objectMapping is the appropriate mapping. + @param fetchRequestBlock A block that accepts an individual resourcePath and returns an NSFetchRequest + that should be used to fetch the local objects associated with resourcePath from CoreData, for use in + properly processing local deletes + @see RKPathMatcher + @see RKURL + @see RKObjectLoader + */ +- (void)setObjectMapping:(RKObjectMappingDefinition *)objectMapping forResourcePathPattern:(NSString *)resourcePathPattern withFetchRequestBlock:(RKObjectMappingProviderFetchRequestBlock)fetchRequestBlock; + +/** + 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. + */ +- (NSFetchRequest *)fetchRequestForResourcePath:(NSString *)resourcePath; + +@end diff --git a/Code/CoreData/RKObjectMappingProvider+CoreData.m b/Code/CoreData/RKObjectMappingProvider+CoreData.m new file mode 100644 index 0000000000..efdd421de2 --- /dev/null +++ b/Code/CoreData/RKObjectMappingProvider+CoreData.m @@ -0,0 +1,31 @@ +// +// RKObjectMappingProvider+CoreData.m +// RestKit +// +// Created by Jeff Arena on 1/26/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingProvider+CoreData.h" +#import "RKOrderedDictionary.h" +#import "RKFixCategoryBug.h" + +RK_FIX_CATEGORY_BUG(RKObjectMappingProvider_CoreData) +@implementation RKObjectMappingProvider (CoreData) + +- (void)setObjectMapping:(RKObjectMappingDefinition *)objectMapping forResourcePathPattern:(NSString *)resourcePath withFetchRequestBlock:(RKObjectMappingProviderFetchRequestBlock)fetchRequestBlock { + [self setEntry:[RKObjectMappingProviderContextEntry contextEntryWithMapping:objectMapping + userData:Block_copy(fetchRequestBlock)] forResourcePathPattern:resourcePath]; +} + +- (NSFetchRequest *)fetchRequestForResourcePath:(NSString *)resourcePath { + RKObjectMappingProviderContextEntry *entry = [self entryForResourcePath:resourcePath]; + if (entry.userData) { + NSFetchRequest *(^fetchRequestBlock)(NSString *) = entry.userData; + return fetchRequestBlock(resourcePath); + } + + return nil; +} + +@end diff --git a/Code/CoreData/RKObjectPropertyInspector+CoreData.h b/Code/CoreData/RKObjectPropertyInspector+CoreData.h index b2af81b115..c04d05dc55 100644 --- a/Code/CoreData/RKObjectPropertyInspector+CoreData.h +++ b/Code/CoreData/RKObjectPropertyInspector+CoreData.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/14/11. -// Copyright 2011 RestKit -// +// 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 1014a4709f..9767b6924a 100644 --- a/Code/CoreData/RKObjectPropertyInspector+CoreData.m +++ b/Code/CoreData/RKObjectPropertyInspector+CoreData.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/14/11. -// Copyright 2011 RestKit -// +// 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 "RKObjectPropertyInspector+CoreData.h" #import "RKLog.h" #import "RKFixCategoryBug.h" +#import + RK_FIX_CATEGORY_BUG(RKObjectPropertyInspector_CoreData) @@ -33,16 +35,35 @@ @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]; - [propertyNamesAndTypes setValue:NSClassFromString([attributeDescription attributeValueClassName]) forKey:name]; + if ([attributeDescription attributeValueClassName]) { + [propertyNamesAndTypes setValue:NSClassFromString([attributeDescription attributeValueClassName]) forKey:name]; + + } else if ([attributeDescription attributeType] == NSTransformableAttributeType && + ![name isEqualToString:@"_mapkit_hasPanoramaID"]) { + + const char* className = [[entity managedObjectClassName] cStringUsingEncoding:NSUTF8StringEncoding]; + const char* propertyName = [name cStringUsingEncoding:NSUTF8StringEncoding]; + Class managedObjectClass = objc_getClass(className); + + // 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 = class_getProperty(managedObjectClass, propertyName); + NSString* attributeString = [NSString stringWithCString:property_getAttributes(prop) encoding:NSUTF8StringEncoding]; + const char* destinationClassName = [[RKObjectPropertyInspector propertyTypeFromAttributeString:attributeString] cStringUsingEncoding:NSUTF8StringEncoding]; + Class destinationClass = objc_getClass(destinationClassName); + if (destinationClass) { + [propertyNamesAndTypes setObject:destinationClass forKey:name]; + } + } } - + for (NSString* name in [entity relationshipsByName]) { NSRelationshipDescription* relationshipDescription = [[entity relationshipsByName] valueForKey:name]; if ([relationshipDescription isToMany]) { @@ -53,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/RKSearchWord.h b/Code/CoreData/RKSearchWord.h new file mode 100644 index 0000000000..e030fff75d --- /dev/null +++ b/Code/CoreData/RKSearchWord.h @@ -0,0 +1,40 @@ +// +// RKSearchWord.m +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "NSManagedObject+ActiveRecord.h" +#import "RKSearchableManagedObject.h" + +extern NSString * const RKSearchWordPrimaryKeyAttribute; + +@interface RKSearchWord : NSManagedObject + +@property (nonatomic, retain) NSString* word; +@property (nonatomic, retain) NSSet* searchableManagedObjects; + +@end + +@interface RKSearchWord (SearchableManagedObjectsAccessors) + +- (void)addSearchableManagedObjectsObject:(RKSearchableManagedObject*)searchableManagedObject; +- (void)removeSearchableManagedObjectsObject:(RKSearchableManagedObject*)searchableManagedObject; +- (void)addSearchableManagedObjects:(NSSet*)searchableManagedObjects; +- (void)removeSearchableManagedObjects:(NSSet*)searchableManagedObjects; + +@end diff --git a/Code/CoreData/RKSearchWord.m b/Code/CoreData/RKSearchWord.m new file mode 100644 index 0000000000..76ae2baf28 --- /dev/null +++ b/Code/CoreData/RKSearchWord.m @@ -0,0 +1,35 @@ +// +// RKSearchWord.m +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKSearchWord.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + +NSString * const RKSearchWordPrimaryKeyAttribute = @"word"; + +@implementation RKSearchWord + +@dynamic word; +@dynamic searchableManagedObjects; + +@end diff --git a/Code/CoreData/RKSearchWordObserver.h b/Code/CoreData/RKSearchWordObserver.h new file mode 100644 index 0000000000..ba5fa01d4b --- /dev/null +++ b/Code/CoreData/RKSearchWordObserver.h @@ -0,0 +1,34 @@ +// +// RKSearchWordObserver.h +// RestKit +// +// Created by Blake Watters on 7/25/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +/** + Provides an observer responsible for initiating refresh of searchable + Core Data attributes at managed object context save time. + */ +@interface RKSearchWordObserver : NSObject + +/** + Returns the shared observer + */ ++ (RKSearchWordObserver *)sharedObserver; + +@end diff --git a/Code/CoreData/RKSearchWordObserver.m b/Code/CoreData/RKSearchWordObserver.m new file mode 100644 index 0000000000..03e040c6bb --- /dev/null +++ b/Code/CoreData/RKSearchWordObserver.m @@ -0,0 +1,70 @@ +// +// RKSearchWordObserver.m +// RestKit +// +// Created by Blake Watters on 7/25/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "RKSearchWordObserver.h" +#import "RKSearchableManagedObject.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreDataSearchEngine + +static RKSearchWordObserver *sharedSearchWordObserver = nil; + +@implementation RKSearchWordObserver + ++ (RKSearchWordObserver *)sharedObserver { + if (! sharedSearchWordObserver) { + sharedSearchWordObserver = [[RKSearchWordObserver alloc] init]; + } + + return sharedSearchWordObserver; +} + +- (id)init { + self = [super init]; + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(managedObjectContextWillSaveNotification:) + name:NSManagedObjectContextWillSaveNotification + object:nil]; + } + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)managedObjectContextWillSaveNotification:(NSNotification *)notification { + NSManagedObjectContext *context = [notification object]; + NSSet *candidateObjects = [[NSSet setWithSet:context.insertedObjects] setByAddingObjectsFromSet:context.updatedObjects]; + + RKLogDebug(@"Managed object context will save notification received. Checking changed and inserted objects for searchable entities..."); + + for (NSManagedObject *object in candidateObjects) { + if (! [object isKindOfClass:[RKSearchableManagedObject class]]) { + RKLogTrace(@"Skipping search words refresh for entity of type '%@': not searchable.", NSStringFromClass([object class])); + continue; + } + + NSArray *searchableAttributes = [[object class] searchableAttributes]; + for (NSString *attribute in searchableAttributes) { + if ([[object changedValues] objectForKey:attribute]) { + RKLogDebug(@"Detected change to searchable attribute '%@' for %@ entity: refreshing search words.", attribute, NSStringFromClass([object class])); + [(RKSearchableManagedObject *)object refreshSearchWords]; + break; + } + } + } +} + +@end diff --git a/Code/CoreData/RKSearchableManagedObject.h b/Code/CoreData/RKSearchableManagedObject.h new file mode 100644 index 0000000000..1b77767e2e --- /dev/null +++ b/Code/CoreData/RKSearchableManagedObject.h @@ -0,0 +1,119 @@ +// +// RKSearchableManagedObject.h +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "NSManagedObject+ActiveRecord.h" +#import "RKManagedObjectSearchEngine.h" + +@class RKSearchWord; + +/** + RKSearchableManagedObject provides an abstract base class for Core Data entities + 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 + */ +@interface RKSearchableManagedObject : NSManagedObject + +///----------------------------------------------------------------------------- +/// @name Configuring Searchable Attributes +///----------------------------------------------------------------------------- + +/** + Returns an array of attributes which should be processed by the search word observer to + 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 + */ ++ (NSArray *)searchableAttributes; + +///----------------------------------------------------------------------------- +/// @name Obtaining a Search Predicate +///----------------------------------------------------------------------------- + +/** + 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. + @see RKSearchMode + */ + ++ (NSPredicate *)predicateForSearchWithText:(NSString *)searchText searchMode:(RKSearchMode)mode; + +///----------------------------------------------------------------------------- +/// @name Managing the Search Words +///----------------------------------------------------------------------------- + +/** + The set of tokenized search words contained in the receiver. + */ +@property (nonatomic, retain) NSSet *searchWords; + +/** + 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; + +@end + +@interface RKSearchableManagedObject (SearchWordsAccessors) + +/** + 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; + +@end diff --git a/Code/CoreData/RKSearchableManagedObject.m b/Code/CoreData/RKSearchableManagedObject.m new file mode 100644 index 0000000000..df667e91c3 --- /dev/null +++ b/Code/CoreData/RKSearchableManagedObject.m @@ -0,0 +1,81 @@ +// +// RKSearchableManagedObject.m +// RestKit +// +// Created by Jeff Arena on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKSearchableManagedObject.h" +#import "CoreData.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreDataSearchEngine + +@implementation RKSearchableManagedObject + +@dynamic searchWords; + ++ (NSArray *)searchableAttributes +{ + return [NSArray array]; +} + ++ (NSPredicate *)predicateForSearchWithText:(NSString *)searchText searchMode:(RKSearchMode)mode +{ + if (searchText == nil) { + return nil; + } else { + RKManagedObjectSearchEngine *searchEngine = [RKManagedObjectSearchEngine searchEngine]; + searchEngine.mode = mode; + return [searchEngine predicateForSearch:searchText]; + } +} + +- (void)refreshSearchWords +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + RKLogDebug(@"Refreshing search words for %@ %@", NSStringFromClass([self class]), [self objectID]); + NSMutableSet *searchWords = [NSMutableSet set]; + for (NSString *searchableAttribute in [[self class] searchableAttributes]) { + NSString *attributeValue = [self valueForKey:searchableAttribute]; + if (attributeValue) { + 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 + inContext:self.managedObjectContext]; + if (! searchWord) { + searchWord = [RKSearchWord createInContext:self.managedObjectContext]; + } + searchWord.word = word; + [searchWords addObject:searchWord]; + } + } + } + } + + self.searchWords = searchWords; + RKLogTrace(@"Generating searchWords: %@", [searchWords valueForKey:RKSearchWordPrimaryKeyAttribute]); + + [pool drain]; +} + +@end diff --git a/Specs/ObjectMapping/RKObjectiveCPlusPlusSpec.mm b/Code/Network/NSData+RKAdditions.h similarity index 62% rename from Specs/ObjectMapping/RKObjectiveCPlusPlusSpec.mm rename to Code/Network/NSData+RKAdditions.h index 00b49f2a7e..b33524a578 100644 --- a/Specs/ObjectMapping/RKObjectiveCPlusPlusSpec.mm +++ b/Code/Network/NSData+RKAdditions.h @@ -1,16 +1,16 @@ // -// RKObjectiveCPlusPlusSpec.mm +// NSData+RKAdditions.h // RestKit // -// Created by Blake Watters on 7/28/11. -// Copyright 2011 RestKit -// +// 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. @@ -18,18 +18,16 @@ // limitations under the License. // - -#import "RestKit.h" - /** - NOTE: - - This is a simple class that ensures RestKit will import cleanly into a Objective-C++ - source files. + Provides extensions to NSData for various common tasks. */ -@interface RKObjectiveCPlusPlusSpec : NSObject +@interface NSData (RKAdditions) -@end +/** + Returns a string of the MD5 sum of the receiver. + + @return A new string containing the MD5 sum of the receiver. + */ +- (NSString *)MD5; -@implementation RKObjectiveCPlusPlusSpec @end diff --git a/Code/Network/NSData+MD5.m b/Code/Network/NSData+RKAdditions.m similarity index 52% rename from Code/Network/NSData+MD5.m rename to Code/Network/NSData+RKAdditions.m index c6d1fa4c40..5ae5fa3ee2 100644 --- a/Code/Network/NSData+MD5.m +++ b/Code/Network/NSData+RKAdditions.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// +// 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,27 +19,27 @@ // #import -#import "NSData+MD5.h" +#import "NSData+RKAdditions.h" #import "RKFixCategoryBug.h" -RK_FIX_CATEGORY_BUG(NSData_MD5) +RK_FIX_CATEGORY_BUG(NSData_RKAdditions) -@implementation NSData (MD5) +@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 984ad7ab28..a2b5505186 100644 --- a/Code/Network/NSDictionary+RKRequestSerialization.h +++ b/Code/Network/NSDictionary+RKRequestSerialization.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 Two Toasters -// +// 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,25 +21,17 @@ #import #import "RKRequestSerializable.h" -/** +/* Extends NSDictionary to enable usage as the params of an RKRequest. - - This protocol provides a serialization of NSDictionary into a URL - encoded string representation. This enables us to provide an NSDictionary - as the params argument for 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 + enables NSDictionary objects to act as the params for an RKRequest. + @see RKRequestSerializable @see [RKRequest params] @class NSDictionary (RKRequestSerialization) */ @interface NSDictionary (RKRequestSerialization) -/** - 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; -- (NSString *)URLEncodedString; // TODO: Deprecated.. - @end diff --git a/Code/Network/NSDictionary+RKRequestSerialization.m b/Code/Network/NSDictionary+RKRequestSerialization.m index 094ad24c82..0c5c19fb9b 100644 --- a/Code/Network/NSDictionary+RKRequestSerialization.m +++ b/Code/Network/NSDictionary+RKRequestSerialization.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 Two Toasters -// +// 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,59 +19,22 @@ // #import "NSDictionary+RKRequestSerialization.h" -#import "NSString+RestKit.h" +#import "NSDictionary+RKAdditions.h" #import "RKFixCategoryBug.h" +#import "RKMIMETypes.h" RK_FIX_CATEGORY_BUG(NSDictionary_RKRequestSerialization) - @implementation NSDictionary (RKRequestSerialization) -- (void)URLEncodePart:(NSMutableArray*)parts path:(NSString*)path value:(id)value { - NSString *encodedPart = [[value description] stringByAddingURLEncoding]; - [parts addObject:[NSString stringWithFormat: @"%@=%@", path, encodedPart]]; -} - -- (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) { - 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]; - } - else { - [self URLEncodePart:parts path:path value:value]; - } - }]; -} - -// TODO: Move to NSDictionary+RestKit -- (NSString *)stringWithURLEncodedEntries { - NSMutableArray* parts = [NSMutableArray array]; - [self URLEncodeParts:parts path:nil]; - return [parts componentsJoinedByString:@"&"]; -} - -- (NSString *)URLEncodedString { - return [self stringWithURLEncodedEntries]; -} - -- (NSString *)HTTPHeaderValueForContentType { - return @"application/x-www-form-urlencoded"; +- (NSString *)HTTPHeaderValueForContentType +{ + return RKMIMETypeFormURLEncoded; } -- (NSData*)HTTPBody { - return [[self URLEncodedString] dataUsingEncoding:NSUTF8StringEncoding]; +- (NSData *)HTTPBody +{ + return [[self URLEncodedString] dataUsingEncoding:NSUTF8StringEncoding]; } @end diff --git a/Code/Network/NSObject+URLEncoding.h b/Code/Network/NSObject+URLEncoding.h new file mode 100644 index 0000000000..5564221db5 --- /dev/null +++ b/Code/Network/NSObject+URLEncoding.h @@ -0,0 +1,19 @@ +// +// NSObject+URLEncoding.h +// RestKit +// +// Created by Jeff Arena on 7/11/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + + +@interface NSObject (URLEncoding) + +/** + * Returns a representation of the object as a URLEncoded string + * + * @returns A UTF-8 encoded string representation of the object + */ +- (NSString*)URLEncodedString; + +@end diff --git a/Code/Network/NSObject+URLEncoding.m b/Code/Network/NSObject+URLEncoding.m new file mode 100644 index 0000000000..54c8a927c3 --- /dev/null +++ b/Code/Network/NSObject+URLEncoding.m @@ -0,0 +1,24 @@ +// +// NSObject+URLEncoding.m +// RestKit +// +// Created by Jeff Arena on 7/11/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "NSObject+URLEncoding.h" + + +@implementation NSObject (URLEncoding) + +- (NSString*)URLEncodedString { + NSString *string = [NSString stringWithFormat:@"%@", self]; + NSString *encodedString = (NSString*)CFURLCreateStringByAddingPercentEscapes(NULL, + (CFStringRef)string, + NULL, + (CFStringRef)@"!*'();:@&=+$,/?%#[]", + kCFStringEncodingUTF8); + return [encodedString autorelease]; +} + +@end diff --git a/Code/Network/NSString+MD5.m b/Code/Network/NSString+MD5.m deleted file mode 100644 index e2f182d098..0000000000 --- a/Code/Network/NSString+MD5.m +++ /dev/null @@ -1,48 +0,0 @@ -// -// NSString+MD5.m -// RestKit -// -// Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// -// 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 -#import "NSString+MD5.h" -#import "RKFixCategoryBug.h" - -RK_FIX_CATEGORY_BUG(NSString_MD5) - -@implementation NSString (MD5) - -- (NSString*)MD5 { - // Create pointer to the string as UTF8 - const char* ptr = [self UTF8String]; - - // Create byte array of unsigned chars - unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH]; - - // Create 16 byte MD5 hash value, store in buffer - CC_MD5(ptr, (CC_LONG) strlen(ptr), md5Buffer); - - // Convert MD5 value in the 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; -} - -@end diff --git a/Code/Network/Network.h b/Code/Network/Network.h index 53ef45f420..46162f2ee9 100644 --- a/Code/Network/Network.h +++ b/Code/Network/Network.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/30/10. -// Copyright 2010 Two Toasters -// +// 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 6676a02654..596481099f 100644 --- a/Code/Network/RKClient.h +++ b/Code/Network/RKClient.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 RestKit -// +// 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. @@ -18,6 +18,7 @@ // limitations under the License. // +#import "RKURL.h" #import "RKRequest.h" #import "RKParams.h" #import "RKResponse.h" @@ -25,562 +26,564 @@ #import "RKReachabilityObserver.h" #import "RKRequestCache.h" #import "RKRequestQueue.h" +#import "RKConfigurationDelegate.h" -///////////////////////////////////////////////////////////////////////// /** - @name URL & URL Path Convenience methods - */ + 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. -/** - 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"]` - - @param resourcePath The resource path to append to the baseURL of the [RKClient sharedClient] - @return A fully constructed NSURL consisting of baseURL of the shared client singleton and the supplied - */ -NSURL *RKMakeURL(NSString *resourcePath); + 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. -/** - 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"] - */ -NSString *RKMakeURLPath(NSString *resourcePath); -/** - 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. 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. - @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 actual property values. - @see RKMakePathWithObjectAddingEscapes - */ -NSString *RKMakePathWithObject(NSString *path, id object); + ### Base URL and Resource Paths -/** - 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. - @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 property values. - @return A new path string, replacing the pattern's parameters with the object's actual property values. - */ -NSString *RKMakePathWithObjectAddingEscapes(NSString *pattern, id object, BOOL addEscapes); + 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 + instances are configured with a Base URL and all requests dispatched through + 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. -/** - 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 - - *NOTE* - Assumes that the resource path does not already contain any query parameters. - - @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 - @return A new resource path with the query parameters appended - */ -NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryParams); - -///////////////////////////////////////////////////////////////////////// - -/** - 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 - instances are configured with a Base URL and all requests dispatched through 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 URL's 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 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. - @see RKRequest @see RKResponse @see RKRequestQueue @see RKRequestSerializable */ -@interface RKClient : NSObject { - NSString *_baseURL; +@interface RKClient : NSObject { + 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; } -///////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- +/// @name Initializing a Client +///----------------------------------------------------------------------------- + +/** + 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. + @return A configured RKClient instance ready to send requests + */ ++ (RKClient *)clientWithBaseURL:(NSURL *)baseURL; + +/** + 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. + @return A configured RKClient instance ready to send requests + */ ++ (RKClient *)clientWithBaseURLString:(NSString *)baseURLString; + +/** + Returns a Rest client scoped to a particular base URL with a set of HTTP AUTH + 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 + to this base URL. + @param username The username to use for HTTP Authentication challenges + @param password The password to use for HTTP Authentication challenges + @return A configured RKClient instance ready to send requests + */ ++ (RKClient *)clientWithBaseURL:(NSString *)baseURL username:(NSString *)username password:(NSString *)password DEPRECATED_ATTRIBUTE; + +/** + 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. + @return A configured RKClient instance ready to send requests + */ +- (id)initWithBaseURL:(NSURL *)baseURL; + +/** + 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. + @return A configured RKClient instance ready to send requests + */ +- (id)initWithBaseURLString:(NSString *)baseURLString; + + +///----------------------------------------------------------------------------- /// @name Configuring the Client -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** 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. - + + 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) NSString *baseURL; +@property (nonatomic, retain) RKURL *baseURL; /** 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. + 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; /** - Accept all SSL certificates. This is a potential security exposure, - and should be used ONLY while debugging in a controlled environment. - - *Default*: _NO_ + The request queue to push asynchronous requests onto. + + *Default*: A new request queue is instantiated for you during init. */ -@property (nonatomic, assign) BOOL disableCertificateValidation; +@property (nonatomic, retain) RKRequestQueue *requestQueue; /** - The request queue to push asynchronous requests onto. - - *Default*: A new request queue is instantiated for you during init + The run loop mode under which the underlying NSURLConnection is performed + + *Default*: NSRunLoopCommonModes */ -@property (nonatomic, retain) RKRequestQueue *requestQueue; +@property (nonatomic, copy) NSString *runLoopMode; + /** - Convenience method for returning the current reachability status - from the reachabilityObserver. - - Equivalent to executing `[[RKClient sharedClient] isNetworkReachable]` - - @see baseURL - @see RKReachabilityObserver - @return YES if the remote host is accessible + 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 */ -- (BOOL)isNetworkReachable; -- (BOOL)isNetworkAvailable DEPRECATED_ATTRIBUTE; +@property (nonatomic, assign) NSStringEncoding defaultHTTPEncoding; /** - * 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 + 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 */ - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)header; -///////////////////////////////////////////////////////////////////////// -/// @name SSL Validation -///////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- +/// @name Handling SSL Validation +///----------------------------------------------------------------------------- + +/** + 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. + */ +@property (nonatomic, assign) BOOL disableCertificateValidation; + /** - * A set of additional certificates to be used in evaluating server - * SSL certificates. + 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 HTTP header to add - * @see 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 */ - (void)addRootCertificate:(SecCertificateRef)cert; -///////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- /// @name Authentication -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** The type of authentication to use for this request. - - When configured to RKRequestAuthenticationTypeHTTPBasic, RestKit will add - an Authorization header establishing login via HTTP Basic. This is an optimization - that skips the challenge portion of the request. - + + This must be assigned one of the following: + + - `RKRequestAuthenticationTypeNone`: Disable the use of authentication + - `RKRequestAuthenticationTypeHTTP`: Use NSURLConnection's HTTP AUTH + auto-negotiation + - `RKRequestAuthenticationTypeHTTPBasic`: 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. + - `RKRequestAuthenticationTypeOAuth1`: Enable the use of OAuth 1.0 + authentication. OAuth1ConsumerKey, OAuth1ConsumerSecret, OAuth1AccessToken, + and OAuth1AccessTokenSecret must be set. + - `RKRequestAuthenticationTypeOAuth2`: Enable the use of OAuth 2.0 + authentication. OAuth2AccessToken must be set. + **Default**: RKRequestAuthenticationTypeNone - - @see RKRequestAuthenticationType + */ @property (nonatomic, assign) RKRequestAuthenticationType authenticationType; /** - The username to use for authentication via HTTP AUTH - + The username to use for authentication via HTTP AUTH. + Used to respond to an authentication challenge when authenticationType is - RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic - + RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic, retain) NSString *username; +@property (nonatomic, retain) NSString *username; /** - The password to use for authentication via HTTP AUTH - + The password to use for authentication via HTTP AUTH. + Used to respond to an authentication challenge when authenticationType is - RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic - + RKRequestAuthenticationTypeHTTP or RKRequestAuthenticationTypeHTTPBasic. + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic, retain) NSString *password; +@property (nonatomic, retain) NSString *password; -/*** @name OAuth Secrets */ + +///----------------------------------------------------------------------------- +/// @name OAuth1 Secrets +///----------------------------------------------------------------------------- /** The OAuth 1.0 consumer key - - Used to build an Authorization header wjem authenticationType is + + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth1ConsumerKey; +@property (nonatomic, retain) NSString *OAuth1ConsumerKey; /** The OAuth 1.0 consumer secret - - Used to build an Authorization header wjem authenticationType is + + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth1ConsumerSecret; +@property (nonatomic, retain) NSString *OAuth1ConsumerSecret; /** The OAuth 1.0 access token - - Used to build an Authorization header wjem authenticationType is + + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth1AccessToken; +@property (nonatomic, retain) NSString *OAuth1AccessToken; /** The OAuth 1.0 access token secret - - Used to build an Authorization header wjem authenticationType is + + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth1 - + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth1AccessTokenSecret; +@property (nonatomic, retain) NSString *OAuth1AccessTokenSecret; + +///----------------------------------------------------------------------------- +/// @name OAuth2 Secrets +///----------------------------------------------------------------------------- -/*** @name OAuth2 Secrets */ /** The OAuth 2.0 access token - - Used to build an Authorization header wjem authenticationType is + + Used to build an Authorization header when authenticationType is RKRequestAuthenticationTypeOAuth2 - + @see authenticationType - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth2AccessToken; +@property (nonatomic, retain) NSString *OAuth2AccessToken; /** - The OAuth 2.0 refresh token. Used to retrieve a new access token before expiration - - Used to build an Authorization header wjem authenticationType is + 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 - @see RKRequestAuthenticationType */ -@property(nonatomic,retain) NSString *OAuth2RefreshToken; +@property (nonatomic, retain) NSString *OAuth2RefreshToken; -///////////////////////////////////////////////////////////////////////// -/// @name Reachability & Service Availability Alerting -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- +/// @name Reachability & Service Availability Alerting +///----------------------------------------------------------------------------- /** - 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. - - Changing the reachability observer has the side-effect of temporarily suspending the - requestQueue until reachability to the new host can be established. - + + 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; +@property (nonatomic, retain) RKReachabilityObserver *reachabilityObserver; /** The title to use in the alert shown when a request encounters a ServiceUnavailable (503) response. - + *Default*: _"Service Unavailable"_ */ -@property(nonatomic, retain) NSString *serviceUnavailableAlertTitle; +@property (nonatomic, retain) NSString *serviceUnavailableAlertTitle; /** 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; +@property (nonatomic, retain) NSString *serviceUnavailableAlertMessage; /** Flag that determines whether the Service Unavailable alert is shown in response to a ServiceUnavailable (503) response. - + *Default*: _NO_ */ -@property(nonatomic, assign) BOOL serviceUnavailableAlertEnabled; +@property (nonatomic, assign) BOOL serviceUnavailableAlertEnabled; -///////////////////////////////////////////////////////////////////////// -/// @name Cacheing -///////////////////////////////////////////////////////////////////////// -/** - An instance of the request cache used to store/load cacheable responses for requests - sent through this client - */ -@property (nonatomic, retain) RKRequestCache *cache DEPRECATED_ATTRIBUTE; +///----------------------------------------------------------------------------- +/// @name Reachability helpers +///----------------------------------------------------------------------------- /** - An instance of the request cache used to store/load cacheable responses for requests - sent through this client - */ -@property (nonatomic, retain) RKRequestCache *requestCache; + Convenience method for returning the current reachability status from the + reachabilityObserver. -/** - The default cache policy to apply for all requests sent through this client - - @see RKRequestCache + Equivalent to executing `[RKClient isNetworkReachable]` on the sharedClient + + @see RKReachabilityObserver + @return YES if the remote host is accessible */ -@property (nonatomic, assign) RKRequestCachePolicy cachePolicy; +- (BOOL)isNetworkReachable; /** - The path used to store response data for this client's request cache + Convenience method for returning the current reachability status from the + reachabilityObserver. + + @bug **DEPRECATED** in v0.10.0: Use [RKClient isNetworkReachable] + @see RKReachabilityObserver + @return YES if the remote host is accessible */ -@property (nonatomic, readonly) NSString *cachePath; +- (BOOL)isNetworkAvailable DEPRECATED_ATTRIBUTE; -///////////////////////////////////////////////////////////////////////// -/// @name Shared Client Instance -///////////////////////////////////////////////////////////////////////// -/** - Return the shared instance of the client - */ -+ (RKClient *)sharedClient; +///----------------------------------------------------------------------------- +/// @name Caching +///----------------------------------------------------------------------------- /** - Set 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; + An instance of the request cache used to store/load cacheable responses for + requests sent through this client -///////////////////////////////////////////////////////////////////////// -/// @name Initializing a Client -///////////////////////////////////////////////////////////////////////// + @bug **DEPRECATED** in v0.10.0: Use requestCache instead. + */ +@property (nonatomic, retain) RKRequestCache *cache DEPRECATED_ATTRIBUTE; /** - Return a client scoped to a particular base URL. If the singleton client is nil, the return client is set as the singleton - - @param baseURL The baseURL to set for the client. All requests will be relative to this base URL - @see baseURL - @return A configured RKClient instance ready to send requests + An instance of the request cache used to store/load cacheable responses for + requests sent through this client */ -+ (RKClient *)clientWithBaseURL:(NSString *)baseURL; +@property (nonatomic, retain) RKRequestCache *requestCache; /** - Return a Rest client scoped to a particular base URL with a set of HTTP AUTH credentials. - If the [singleton client]([RKClient sharedClient]) is nil, the client instantiated will become the singleton instance - - @param baseURL The baseURL to set for the client. All requests will be relative to this base URL - @param username The username to use for HTTP Authentication challenges - @param password The password to use for HTTP Authentication challenges - @return A configured RKClient instance ready to send requests + 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. */ -+ (RKClient *)clientWithBaseURL:(NSString *)baseURL username:(NSString *)username password:(NSString *)password; +@property (nonatomic, assign) NSTimeInterval cacheTimeoutInterval; /** - Return a client scoped to a particular base URL. If the singleton client is nil, the return client is set as the singleton - - @param baseURL The baseURL to set for the client. All requests will be relative to this base URL - @see baseURL - @return A configured RKClient instance ready to send requests - */ -- (id)initWithBaseURL:(NSString *)baseURL; + The default cache policy to apply for all requests sent through this client -///////////////////////////////////////////////////////////////////////// -/// @name Constructing Resource Paths and URLs -///////////////////////////////////////////////////////////////////////// + This must be assigned one of the following: -/** - Returns a NSURL by adding a resource path to the base URL - - @param resourcePath The resource path to build a URL against - @return An NSURL constructed by concatenating the baseURL and the resourcePath + - `RKRequestCachePolicyNone`: Never use the cache. + - `RKRequestCachePolicyLoadIfOffline`: Load from the cache when offline. + - `RKRequestCachePolicyLoadOnError`: Load from the cache if an error is + encountered. + - `RKRequestCachePolicyEtag`: Load from the cache if there is data stored and + the server returns a 304 (Not Modified) response. + - `RKRequestCachePolicyEnabled`: Load from the cache whenever data has been + stored. + - `RKRequestCachePolicyTimeout`: Load from the cache if the + cacheTimeoutInterval is reached before the server responds. + + @see RKRequest */ -- (NSURL *)URLForResourcePath:(NSString *)resourcePath; +@property (nonatomic, assign) RKRequestCachePolicy cachePolicy; /** - Returns an NSString by adding a resource path to the base URL - - @param resourcePath The resource path to build a URL against - @return A string URL constructed by concatenating the baseURL and the resourcePath + 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. */ -- (NSString *)URLPathForResourcePath:(NSString *)resourcePath; +@property (nonatomic, readonly) NSString *cachePath; + + +///----------------------------------------------------------------------------- +/// @name Shared Client Instance +///----------------------------------------------------------------------------- /** - 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 - - *NOTE* - Assumes that the resource path does not already contain any query parameters. - - @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 - @return A new resource path with the query parameters appended - - @see RKPathAppendQueryParams() - @deprecated Use RKPathAppendQueryParams instead + Returns the shared instance of the client */ -- (NSString *)resourcePath:(NSString *)resourcePath withQueryParams:(NSDictionary *)queryParams DEPRECATED_ATTRIBUTE; ++ (RKClient *)sharedClient; /** - 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 - - *NOTE* - Assumes that the resource path does not already contain any query parameters. - - @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 - @return A URL constructed by concatenating the baseURL and the resourcePath with the query parameters appended + 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 */ -- (NSURL *)URLForResourcePath:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams; ++ (void)setSharedClient:(RKClient *)client; -///////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- /// @name Building Requests -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** - Configures a request with the headers and authentication settings applied to this client - - @param request A request to apply the configuration to - @see HTTPHeaders - @see username - @see password + 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.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. + @see RKRequestDelegate */ -- (void)setupRequest:(RKRequest *)request; +- (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObject *)delegate DEPRECATED_ATTRIBUTE; /** - 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. - - @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 + 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 */ -- (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObject *)delegate; +- (RKRequest *)requestWithResourcePath:(NSString *)resourcePath; -/////////////////////////////////////////////////////////////////////////////////////////////////////////// -/** - * These methods are provided as a convenience to cover the common asynchronous request tasks. All other request - * needs should instantiate a request via requestWithResourcePath:delegate:callback and work with the RKRequest - * object directly. - * - * @name Sending Asynchronous Requests - */ +///----------------------------------------------------------------------------- +/// @name Sending Asynchronous Requests +///----------------------------------------------------------------------------- /** - Perform an asynchronous GET request for a resource and inform a delegate of the results - + 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 @@ -588,21 +591,35 @@ NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryPar - (RKRequest *)get:(NSString *)resourcePath delegate:(NSObject *)delegate; /** - Fetch a resource via an HTTP GET with a dictionary of params - - Note that 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. - + 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 queryParams A dictionary of query parameters to append to the resourcePath. Assumes that resourcePath does not contain a query string. + @param queryParameters A dictionary of query parameters to append to the + resourcePath. Assumes that resourcePath does not contain a query string. @param delegate A delegate object to inform of the results @return The RKRequest object built and sent to the remote system */ -- (RKRequest *)get:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams delegate:(NSObject *)delegate; +- (RKRequest *)get:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters delegate:(NSObject *)delegate; + +/** + 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. + */ +- (void)get:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block; /** - Create a resource via an HTTP POST with a set of form parameters - + 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 @param delegate A delegate object to inform of the results @@ -612,8 +629,21 @@ NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryPar - (RKRequest *)post:(NSString *)resourcePath params:(NSObject *)params delegate:(NSObject *)delegate; /** - Update a resource via an HTTP PUT - + 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. + */ +- (void)post:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block; + +/** + Update a resource via an HTTP PUT. + + 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 @@ -623,12 +653,198 @@ NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryPar - (RKRequest *)put:(NSString *)resourcePath params:(NSObject *)params delegate:(NSObject *)delegate; /** - Destroy a resource via an HTTP DELETE - + 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. + */ +- (void)put:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block; + +/** + 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 */ - (RKRequest *)delete:(NSString *)resourcePath delegate:(NSObject *)delegate; +/** + 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. + */ +- (void)delete:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block; + +///----------------------------------------------------------------------------- +/// @name Constructing Resource Paths and URLs +///----------------------------------------------------------------------------- + +/** + Returns a NSURL by adding a resource path to the base URL + + @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 + */ +- (NSURL *)URLForResourcePath:(NSString *)resourcePath DEPRECATED_ATTRIBUTE; + +/** + 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. + */ +- (NSString *)URLPathForResourcePath:(NSString *)resourcePath DEPRECATED_ATTRIBUTE; + +/** + 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. + @return A new resource path with the query parameters appended + */ +- (NSString *)resourcePath:(NSString *)resourcePath withQueryParams:(NSDictionary *)queryParams DEPRECATED_ATTRIBUTE; + +/** + 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. + @return A URL constructed by concatenating the baseURL and the resourcePath + with the query parameters appended. + */ +- (NSURL *)URLForResourcePath:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams DEPRECATED_ATTRIBUTE; + @end + + +///----------------------------------------------------------------------------- +/// @name URL & URL Path Convenience methods +///----------------------------------------------------------------------------- + +/** + 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.10.0: Use [[RKClient sharedClient].baseURL + URLByAppendingResourcePath:] + @param resourcePath The resource path to append to the baseURL of the + `[RKClient sharedClient]` + @return A fully constructed NSURL consisting of baseURL of the shared client + singleton and the supplied resource path + */ +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.10.0: Use + [[[RKClient sharedClient].baseURL URLByAppendingResourcePath:] absoluteString] + @param resourcePath The resource path to append to the baseURL of the + `[RKClient sharedClient]` + @return A fully constructed NSURL consisting of baseURL of the shared client + singleton and the supplied resource path + */ +NSString *RKMakeURLPath(NSString *resourcePath) 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. 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.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 + actual property values. + @see RKMakePathWithObjectAddingEscapes + */ +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.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 + property values. + @return A new path string, replacing the pattern's parameters with the object's + actual property values. + */ +NSString *RKMakePathWithObjectAddingEscapes(NSString *pattern, id object, BOOL addEscapes) DEPRECATED_ATTRIBUTE; + +/** + 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.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. + @return A new resource path with the query parameters appended. + */ +NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryParams) DEPRECATED_ATTRIBUTE; diff --git a/Code/Network/RKClient.m b/Code/Network/RKClient.m index ec47c8b563..70eb174e65 100644 --- a/Code/Network/RKClient.m +++ b/Code/Network/RKClient.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 RestKit -// +// 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 @@ #import "RKAlert.h" #import "RKLog.h" #import "RKPathMatcher.h" -#import "NSString+RestKit.h" +#import "NSString+RKAdditions.h" #import "RKDirectory.h" // Set Logging Component @@ -34,17 +34,17 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// // Global -static RKClient* sharedClient = nil; +static RKClient *sharedClient = nil; /////////////////////////////////////////////////////////////////////////////////////////////////// // URL Conveniences functions NSURL *RKMakeURL(NSString *resourcePath) { - return [[RKClient sharedClient] URLForResourcePath:resourcePath]; + return [[RKClient sharedClient].baseURL URLByAppendingResourcePath:resourcePath]; } NSString *RKMakeURLPath(NSString *resourcePath) { - return [[RKClient sharedClient] URLPathForResourcePath:resourcePath]; + return [[[RKClient sharedClient].baseURL URLByAppendingResourcePath:resourcePath] absoluteString]; } NSString *RKMakePathWithObjectAddingEscapes(NSString* pattern, id object, BOOL addEscapes) { @@ -60,14 +60,16 @@ } NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryParams) { - if ([queryParams count] > 0) { - return [NSString stringWithFormat:@"%@?%@", resourcePath, [queryParams stringWithURLEncodedEntries]]; - } - return resourcePath; + return [resourcePath stringByAppendingQueryParameters:queryParams]; } /////////////////////////////////////////////////////////////////////////////////////////////////// +@interface RKClient () +@property (nonatomic, retain, readwrite) NSMutableDictionary *HTTPHeaders; +@property (nonatomic, retain, readwrite) NSSet *additionalRootCertificates; +@end + @implementation RKClient @synthesize baseURL = _baseURL; @@ -91,144 +93,150 @@ @implementation RKClient @synthesize cachePolicy = _cachePolicy; @synthesize requestQueue = _requestQueue; @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 { + return [self clientWithBaseURL:[RKURL URLWithString:baseURLString]]; } -+ (RKClient *)clientWithBaseURL:(NSString *)baseURL { - RKClient *client = [[[self alloc] initWithBaseURL:baseURL] autorelease]; - return client; ++ (RKClient *)clientWithBaseURL:(NSURL *)baseURL { + RKClient *client = [[[self alloc] initWithBaseURL:baseURL] autorelease]; + return client; } + (RKClient *)clientWithBaseURL:(NSString *)baseURL username:(NSString *)username password:(NSString *)password { - RKClient *client = [RKClient clientWithBaseURL: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]; - 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 + if (self) { + self.HTTPHeaders = [NSMutableDictionary dictionary]; + self.additionalRootCertificates = [NSMutableSet set]; + self.defaultHTTPEncoding = NSUTF8StringEncoding; + self.cacheTimeoutInterval = 0; + 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:(NSString *)baseURL { +- (id)initWithBaseURL:(NSURL *)baseURL { self = [self init]; - if (self) { + if (self) { self.cachePolicy = RKRequestCachePolicyDefault; - self.baseURL = baseURL; - + self.baseURL = [RKURL URLWithBaseURL:baseURL]; + if (sharedClient == nil) { [RKClient setSharedClient:self]; - + // Initialize Logging as soon as a client is created RKLogInitialize(); } } - + return self; } -- (void)dealloc { +- (id)initWithBaseURLString:(NSString *)baseURLString { + return [self initWithBaseURL:[RKURL URLWithString:baseURLString]]; +} + +- (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]; } - (NSString *)cachePath { - NSString *cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:self.baseURL] host]]; + NSString *cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", [self.baseURL host]]; NSString *cachePath = [[RKDirectory cachesDirectory] stringByAppendingPathComponent:cacheDirForClient]; return cachePath; } - (BOOL)isNetworkReachable { - BOOL isNetworkReachable = YES; - if (self.reachabilityObserver) { - isNetworkReachable = [self.reachabilityObserver isNetworkReachable]; - } - - return isNetworkReachable; -} - -- (NSString *)resourcePath:(NSString *)resourcePath withQueryParams:(NSDictionary *)queryParams { - return RKPathAppendQueryParams(resourcePath, queryParams); -} - -- (NSURL *)URLForResourcePath:(NSString *)resourcePath { - return [RKURL URLWithBaseURLString:self.baseURL resourcePath:resourcePath]; -} - -- (NSString *)URLPathForResourcePath:(NSString *)resourcePath { - return [[self URLForResourcePath:resourcePath] absoluteString]; -} + BOOL isNetworkReachable = YES; + if (self.reachabilityObserver) { + isNetworkReachable = [self.reachabilityObserver isNetworkReachable]; + } -- (NSURL *)URLForResourcePath:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams { - return [RKURL URLWithBaseURLString:self.baseURL resourcePath:resourcePath queryParams:queryParams]; + return isNetworkReachable; } -- (void)setupRequest:(RKRequest *)request { - request.additionalHTTPHeaders = _HTTPHeaders; +- (void)configureRequest:(RKRequest *)request { + 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)setupRequest:(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; } @@ -283,20 +291,19 @@ - (void)reachabilityObserverDidChange:(NSDictionary *)change { } - (void)baseURLDidChange:(NSDictionary *)change { - NSString *newBaseURLString = [change objectForKey:NSKeyValueChangeNewKey]; - + RKURL *newBaseURL = [change objectForKey:NSKeyValueChangeNewKey]; + // Don't crash if baseURL is nil'd out (i.e. dealloc) - if (! [newBaseURLString isEqual:[NSNull null]]) { + if (! [newBaseURL isEqual:[NSNull null]]) { // Configure a cache for the new base URL [_requestCache release]; - _requestCache = [[RKRequestCache alloc] initWithCachePath:[self cachePath] + _requestCache = [[RKRequestCache alloc] initWithPath:[self cachePath] storagePolicy:RKRequestCacheStoragePolicyPermanently]; - + // Determine reachability strategy (if user has not already done so) if (self.reachabilityObserver == nil) { - NSURL *newBaseURL = [NSURL URLWithString:newBaseURLString]; NSString *hostName = [newBaseURL host]; - if ([newBaseURLString isEqualToString:@"localhost"] || [hostName isIPAddress]) { + if ([hostName isEqualToString:@"localhost"] || [hostName isIPAddress]) { self.reachabilityObserver = [RKReachabilityObserver reachabilityObserverForHost:hostName]; } else { self.reachabilityObserver = [RKReachabilityObserver reachabilityObserverForInternet]; @@ -309,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; } @@ -330,12 +337,19 @@ - (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; +} + - (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObject *)delegate { - RKRequest *request = [[RKRequest alloc] initWithURL:[self URLForResourcePath:resourcePath] delegate:delegate]; - [self setupRequest:request]; - [request autorelease]; + RKRequest *request = [self requestWithResourcePath:resourcePath]; + request.delegate = delegate; - return request; + return request; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -343,43 +357,43 @@ - (RKRequest *)requestWithResourcePath:(NSString *)resourcePath delegate:(NSObje /////////////////////////////////////////////////////////////////////////////////////////////////////////// - (RKRequest *)load:(NSString *)resourcePath method:(RKRequestMethod)method params:(NSObject *)params delegate:(id)delegate { - NSURL* resourcePathURL = nil; - if (method == RKRequestMethodGET) { - resourcePathURL = [self URLForResourcePath:resourcePath queryParams:(NSDictionary*)params]; - } else { - resourcePathURL = [self URLForResourcePath:resourcePath]; - } - RKRequest *request = [[RKRequest alloc] initWithURL:resourcePathURL delegate:delegate]; - [self setupRequest:request]; - [request autorelease]; - request.method = method; - if (method != RKRequestMethodGET) { - request.params = params; - } - + 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]; + request.delegate = delegate; + [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 queryParams:(NSDictionary *)queryParams delegate:(id)delegate { - return [self load:resourcePath method:RKRequestMethodGET params:queryParams delegate:delegate]; +- (RKRequest *)get:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters delegate:(id)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 { @@ -391,13 +405,15 @@ - (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; [[NSNotificationCenter defaultCenter] removeObserver:self name:RKReachabilityWasDeterminedNotification object:observer]; } +#pragma mark - Deprecations + // deprecated - (RKRequestCache *)cache { return _requestCache; @@ -408,9 +424,62 @@ - (void)setCache:(RKRequestCache *)requestCache { self.requestCache = requestCache; } +#pragma mark - Block Request Dispatching + +- (RKRequest *)sendRequestToResourcePath:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block { + RKRequest *request = [self requestWithResourcePath:resourcePath]; + if (block) block(request); + [request send]; + return request; +} + +- (void)get:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block { + [self sendRequestToResourcePath:resourcePath usingBlock:^(RKRequest *request) { + request.method = RKRequestMethodGET; + block(request); + }]; +} + +- (void)post:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block { + [self sendRequestToResourcePath:resourcePath usingBlock:^(RKRequest *request) { + request.method = RKRequestMethodPOST; + block(request); + }]; +} + +- (void)put:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block { + [self sendRequestToResourcePath:resourcePath usingBlock:^(RKRequest *request) { + request.method = RKRequestMethodPUT; + block(request); + }]; +} + +- (void)delete:(NSString *)resourcePath usingBlock:(void (^)(RKRequest *request))block { + [self sendRequestToResourcePath:resourcePath usingBlock:^(RKRequest *request) { + request.method = RKRequestMethodDELETE; + block(request); + }]; +} + // deprecated - (BOOL)isNetworkAvailable { return [self isNetworkReachable]; } +- (NSString *)resourcePath:(NSString *)resourcePath withQueryParams:(NSDictionary *)queryParams { + return RKPathAppendQueryParams(resourcePath, queryParams); +} + +- (NSURL *)URLForResourcePath:(NSString *)resourcePath { + return [self.baseURL URLByAppendingResourcePath:resourcePath]; +} + +- (NSString *)URLPathForResourcePath:(NSString *)resourcePath { + return [[self URLForResourcePath:resourcePath] absoluteString]; +} + +- (NSURL *)URLForResourcePath:(NSString *)resourcePath queryParams:(NSDictionary *)queryParams { + return [self.baseURL URLByAppendingResourcePath:resourcePath queryParameters:queryParams]; +} + @end diff --git a/Code/Network/RKNotifications.h b/Code/Network/RKNotifications.h index b9fb5340de..a96c480b5d 100644 --- a/Code/Network/RKNotifications.h +++ b/Code/Network/RKNotifications.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/24/09. -// Copyright 2009 Two Toasters -// +// 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,17 +20,18 @@ #import -/****************** - * 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 times +/** + 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 + times. */ -extern NSString* const RKRequestSentNotification; -extern NSString* const RKRequestDidLoadResponseNotification; -extern NSString* const RKRequestDidLoadResponseNotificationUserInfoResponseKey; -extern NSString* const RKRequestDidFailWithErrorNotification; -extern NSString* const RKRequestDidFailWithErrorNotificationUserInfoErrorKey; -extern NSString* const RKServiceDidBecomeUnavailableNotification; +extern NSString * const RKRequestSentNotification; +extern NSString * const RKRequestDidLoadResponseNotification; +extern NSString * const RKRequestDidLoadResponseNotificationUserInfoResponseKey; +extern NSString * const RKRequestDidFailWithErrorNotification; +extern NSString * const RKRequestDidFailWithErrorNotificationUserInfoErrorKey; +extern NSString * const RKRequestDidFinishLoadingNotification; +extern NSString * const RKServiceDidBecomeUnavailableNotification; diff --git a/Code/Network/RKNotifications.m b/Code/Network/RKNotifications.m index 1839591ad5..75e9b4ee63 100644 --- a/Code/Network/RKNotifications.m +++ b/Code/Network/RKNotifications.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/24/09. -// Copyright 2009 Two Toasters -// +// 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,9 +20,10 @@ #import "RKNotifications.h" -NSString* const RKRequestSentNotification = @"RKRequestSentNotification"; -NSString* const RKRequestDidFailWithErrorNotification = @"RKRequestDidFailWithErrorNotification"; -NSString* const RKRequestDidFailWithErrorNotificationUserInfoErrorKey = @"error"; -NSString* const RKRequestDidLoadResponseNotification = @"RKRequestDidLoadResponseNotification"; -NSString* const RKRequestDidLoadResponseNotificationUserInfoResponseKey = @"response"; -NSString* const RKServiceDidBecomeUnavailableNotification = @"RKServiceDidBecomeUnavailableNotification"; \ No newline at end of file +NSString * const RKRequestSentNotification = @"RKRequestSentNotification"; +NSString * const RKRequestDidFailWithErrorNotification = @"RKRequestDidFailWithErrorNotification"; +NSString * const RKRequestDidFailWithErrorNotificationUserInfoErrorKey = @"error"; +NSString * const RKRequestDidLoadResponseNotification = @"RKRequestDidLoadResponseNotification"; +NSString * const RKRequestDidLoadResponseNotificationUserInfoResponseKey = @"response"; +NSString * const RKServiceDidBecomeUnavailableNotification = @"RKServiceDidBecomeUnavailableNotification"; +NSString * const RKRequestDidFinishLoadingNotification = @"RKRequestDidFinishLoadingNotification"; diff --git a/Code/Network/RKOAuthClient.h b/Code/Network/RKOAuthClient.h index f75fe1a8aa..1b051d945c 100644 --- a/Code/Network/RKOAuthClient.h +++ b/Code/Network/RKOAuthClient.h @@ -3,14 +3,14 @@ // RestKit // // Created by Rodrigo Garcia on 7/20/11. -// Copyright 2011 RestKit. All rights reserved. -// +// 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,114 +23,294 @@ #import "RKRequest.h" /** - Defines error codes for OAuth client errors + Defines error codes for OAuth client errors that are returned via a callback + to RKOAuthClient's delegate */ typedef enum RKOAuthClientErrors { - RKOAuthClientErrorInvalidGrant = 3001, /* An invalid authorization code was encountered */ - RKOAuthClientErrorUnauthorizedClient = 3002, /* The client is not authorized to perform the action */ - RKOAuthClientErrorInvalidClient = 3003, /* Client authentication failed (e.g. unknown client, no - client authentication included, or unsupported - authentication method). */ - RKOAuthClientErrorInvalidRequest = 3004, /* The request is missing a required parameter, includes an - unsupported parameter value, repeats a parameter, - includes multiple credentials, utilizes more than one - mechanism for authenticating the client, or is otherwise - malformed. */ - RKOAuthClientErrorUnsupportedGrantType = 3005, /* The authorization grant type is not supported by the authorization server. */ - RKOAuthClientErrorInvalidScope = 3006, /* The requested scope is invalid, unknown, malformed, or exceeds the scope - granted by the resource owner. */ - RKOAuthClientErrorRequestFailure = 3007, /* An underlying RKRequest failed due to an error. The userInfo dictionary - will contain an NSUnderlyingErrorKey with the details of the failure */ - RKOAuthClientErrorUnknown = 0 /* Error was encountered and error_description unknown */ + /** + An invalid authorization code was encountered + */ + RKOAuthClientErrorInvalidGrant = 3001, + /** + The client is not authorized to perform the action + */ + RKOAuthClientErrorUnauthorizedClient = 3002, + /** + Client authentication failed (e.g. unknown client, no client + authentication included, or unsupported authentication method). + */ + RKOAuthClientErrorInvalidClient = 3003, + /** + The request is missing a required parameter, includes an unsupported + parameter value, repeats a parameter, includes multiple credentials, + utilizes more than one mechanism for authenticating the client, or is + otherwise malformed. + */ + RKOAuthClientErrorInvalidRequest = 3004, + /** + The authorization grant type is not supported by the authorization server. + */ + RKOAuthClientErrorUnsupportedGrantType = 3005, + /** + The requested scope is invalid, unknown, malformed, or exceeds the scope + granted by the resource owner. + */ + RKOAuthClientErrorInvalidScope = 3006, + /** + An underlying RKRequest failed due to an error. The userInfo dictionary + will contain an NSUnderlyingErrorKey with the details of the failure + */ + RKOAuthClientErrorRequestFailure = 3007, + /** + Error was encountered and error_description unknown + */ + RKOAuthClientErrorUnknown = 0 } RKOAuthClientErrorCode; @protocol RKOAuthClientDelegate; + /** - An OAuth client implementation used for OAuth 2 authorization code flow. - - See http://tools.ietf.org/html/draft-ietf-oauth-v2-22 + 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" + delegate:yourDelegate]; + oauthClient.authorizationCode = @"AUTHORIZATION_CODE"; + 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; +@interface RKOAuthClient : NSObject { + NSString *_clientID; NSString *_clientSecret; - NSString *_authorizationCode; + NSString *_authorizationCode; NSString *_authorizationURL; NSString *_callbackURL; NSString *_accessToken; id _delegate; } -// General properties of the client -@property(nonatomic,retain) NSString *authorizationCode; -// OAuth Client ID and Secret -@property(nonatomic,retain) NSString *clientID; -@property(nonatomic,retain) NSString *clientSecret; +///----------------------------------------------------------------------------- +/// @name Creating an RKOAuthClient +///----------------------------------------------------------------------------- -// OAuth EndPoints -@property(nonatomic,retain) NSString *authorizationURL; -@property(nonatomic,retain) NSString *callbackURL; +/** + 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. + @return An RKOAuthClient initialized with a client ID and secret key. + */ +- (id)initWithClientID:(NSString *)clientID secret:(NSString *)secret; /** - Returns the access token retrieved + Creates and returns an RKOAuthClient initialized 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. + @return An RKOAuthClient initialized with a client ID and secret key. */ -@property (nonatomic, readonly) NSString *accessToken; ++ (RKOAuthClient *)clientWithClientID:(NSString *)clientID secret:(NSString *)secret; -// Client Delegate -@property (nonatomic,assign) id delegate; -- (id)initWithClientID:(NSString *)clientId - secret:(NSString *)secret - delegate:(id)delegate; +///----------------------------------------------------------------------------- +/// @name General properties +///----------------------------------------------------------------------------- + +/** + 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; + + +///----------------------------------------------------------------------------- +/// @name Client credentials +///----------------------------------------------------------------------------- + +/** + The ID of your application obtained from the OAuth provider + */ +@property (nonatomic, retain) NSString *clientID; + +/** + Confidential key obtained from the OAuth provider that is used to sign requests + sent to the authentication server. + */ +@property (nonatomic, retain) NSString *clientSecret; + + +///----------------------------------------------------------------------------- +/// @name Endpoints +///----------------------------------------------------------------------------- + +/** + A string of the URL where the authorization server can be accessed + */ +@property (nonatomic, retain) NSString *authorizationURL; + +/** + A string of the URL where authorization attempts will be redirected to + */ +@property (nonatomic, retain) NSString *callbackURL; -- (void)validateAuthorizationCode; -+ (RKOAuthClient *)clientWithClientID:(NSString *)clientId - secret:(NSString *)secret - delegate:(id)delegate; +///----------------------------------------------------------------------------- +/// @name Working with the authorization flow +///----------------------------------------------------------------------------- + +/** + The authorization code is used in conjunction with your client secret to obtain + an access token. + */ +@property (nonatomic, retain) NSString *authorizationCode; + +/** + Returns the access token retrieved from the authentication server + */ +@property (nonatomic, readonly) NSString *accessToken; + +/** + 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; @end + /** - * Lifecycle events for RKClientOAuth + 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 + authorization code flow. */ @protocol RKOAuthClientDelegate @required +///----------------------------------------------------------------------------- +/// @name Successful responses +///----------------------------------------------------------------------------- + /** - * Sent when a new access token has being acquired + 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. */ - (void)OAuthClient:(RKOAuthClient *)client didAcquireAccessToken:(NSString *)token; + +///----------------------------------------------------------------------------- +/// @name Handling errors +///----------------------------------------------------------------------------- + /** - * Sent when an access token request has failed due an invalid authorization code + 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 */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidGrantError:(NSError *)error; @optional /** - * Other OAuth2 protocol exceptions for the authorization code flow - */ + Sent to the delegate when the OAuth client encounters any error. -/** - 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 */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithError:(NSError *)error; +/** + 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 + */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithUnauthorizedClientError:(NSError *)error; +/** + 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 + */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidClientError:(NSError *)error; +/** + 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 + */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidRequestError:(NSError *)error; +/** + 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 + */ - (void)OAuthClient:(RKOAuthClient *)client didFailWithUnsupportedGrantTypeError:(NSError *)error; +/** + 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 loading due to an 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 + the callback */ - (void)OAuthClient:(RKOAuthClient *)client didFailLoadingRequest:(RKRequest *)request withError:(NSError *)error; diff --git a/Code/Network/RKOAuthClient.m b/Code/Network/RKOAuthClient.m index 3e3aeb77a5..7642f4d912 100644 --- a/Code/Network/RKOAuthClient.m +++ b/Code/Network/RKOAuthClient.m @@ -3,14 +3,14 @@ // RestKit // // Created by Rodrigo Garcia on 7/20/11. -// Copyright 2011 RestKit. All rights reserved. -// +// 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,7 +19,10 @@ // #import "RKOAuthClient.h" -#import "Errors.h" +#import "RKErrors.h" + +@interface RKOAuthClient () +@end @implementation RKOAuthClient @@ -31,24 +34,18 @@ @implementation RKOAuthClient @synthesize delegate = _delegate; @synthesize accessToken = _accessToken; -+ (RKOAuthClient *)clientWithClientID:(NSString *)clientId - secret:(NSString *)secret - delegate:(id)delegate { - RKOAuthClient *client = [[[self alloc] initWithClientID:clientId secret:secret delegate:delegate] autorelease]; ++ (RKOAuthClient *)clientWithClientID:(NSString *)clientID secret:(NSString *)secret { + RKOAuthClient *client = [[[self alloc] initWithClientID:clientID secret:secret] autorelease]; return client; } -- (id)initWithClientID:(NSString *)clientId - secret:(NSString *)secret - delegate:(id)delegate -{ +- (id)initWithClientID:(NSString *)clientID secret:(NSString *)secret { self = [super init]; if (self) { - _clientID = [clientId copy]; + _clientID = [clientID copy]; _clientSecret = [secret copy]; - _delegate = delegate; } - + return self; } @@ -56,15 +53,16 @@ - (void)dealloc { [_clientID release]; [_clientSecret release]; [_accessToken release]; - + [super dealloc]; } - (void)validateAuthorizationCode { NSString *httpBody = [NSString stringWithFormat:@"client_id=%@&client_secret=%@&code=%@&redirect_uri=%@&grant_type=authorization_code", _clientID, _clientSecret, _authorizationCode, _callbackURL]; - RKClient *requestClient = [RKClient clientWithBaseURL:_authorizationURL]; - RKRequest *theRequest = [requestClient requestWithResourcePath:@"" delegate:self]; + NSURL *URL = [NSURL URLWithString:_authorizationURL]; + RKRequest *theRequest = [RKRequest requestWithURL:URL]; + theRequest.delegate = self; [theRequest setHTTPBodyString:httpBody]; [theRequest setMethod:RKRequestMethodPOST]; [theRequest send]; @@ -73,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; } @@ -113,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:RKRestKitErrorDomain code:errorCode userInfo:userInfo]; - - + 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]; @@ -167,11 +165,11 @@ - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { - (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error { NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: error, NSUnderlyingErrorKey, nil]; - NSError *clientError = [NSError errorWithDomain:RKRestKitErrorDomain code:RKOAuthClientErrorRequestFailure userInfo:userInfo]; + NSError *clientError = [NSError errorWithDomain:RKErrorDomain code:RKOAuthClientErrorRequestFailure userInfo:userInfo]; 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 95b44f80e5..0fcc609b1d 100644 --- a/Code/Network/RKParams.h +++ b/Code/Network/RKParams.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/3/09. -// Copyright 2009 Two Toasters -// +// 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,97 +23,188 @@ #import "RKParamsAttachment.h" /** - * Provides support for creating multi-part request body for RKRequest - * objects. + 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 NSMutableArray *_attachments; - NSStreamStatus _streamStatus; - NSData* _footer; - NSUInteger _bytesDelivered; - NSUInteger _length; - NSUInteger _footerLength; - NSUInteger _currentPart; + NSStreamStatus _streamStatus; + NSData *_footer; + NSUInteger _bytesDelivered; + NSUInteger _length; + NSUInteger _footerLength; + NSUInteger _currentPart; } -/** - Array of all attachments - */ -@property (nonatomic, readonly) NSMutableArray *attachments; + +///----------------------------------------------------------------------------- +/// @name Creating an RKParams object +///----------------------------------------------------------------------------- /** - Returns an empty params object ready for population + Creates and returns an RKParams object that is ready for population. + + @return An RKParams object to be populated. */ + (RKParams *)params; /** - Initialize a params object from a dictionary of key/value pairs + 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. */ + (RKParams *)paramsWithDictionary:(NSDictionary *)dictionary; /** - Initalize a params object from a dictionary of key/value pairs + 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. */ - (RKParams *)initWithDictionary:(NSDictionary *)dictionary; + +///----------------------------------------------------------------------------- +/// @name Working with attachments +///----------------------------------------------------------------------------- + /** - Sets the value for a named parameter + Array of all RKParamsAttachment attachments */ -- (RKParamsAttachment *)setValue:(id )value forParam:(NSString *)param; +@property (nonatomic, readonly) NSMutableArray *attachments; /** - Get the dictionary of the params which are: - * 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". - - Source: http://tools.ietf.org/html/rfc5849#section-3.4.1.3 + Creates a new RKParamsAttachment from the key/value pair passed in and adds it + to the attachments array. + + @param value Value of the attachment to add + @param param Key name of the attachment to add + @return the new RKParamsAttachment that was added to the attachments array */ -- (NSDictionary *)dictionaryOfPlainTextParams; +- (RKParamsAttachment *)setValue:(id )value forParam:(NSString *)param; /** - Sets the value for a named parameter to the data contained in a file at the given path + 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 */ - (RKParamsAttachment *)setFile:(NSString *)filePath forParam:(NSString *)param; /** - Sets the value to the data object for a named parameter. A default MIME type of - application/octet-stream will be used. + 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 + */ +- (RKParamsAttachment *)setData:(NSData *)data forParam:(NSString *)param; + +/** + 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 + @return the new RKParamsAttachment that was added to the attachments array */ -- (RKParamsAttachment *)setData:(NSData*)data forParam:(NSString *)param; +- (RKParamsAttachment *)setData:(NSData *)data MIMEType:(NSString *)MIMEType forParam:(NSString *)param; /** - Sets the value for a named parameter to an NSData object with a specific MIME type + 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 + @param param Key name of the attachment to add + @return the new RKParamsAttachment that was added to the attachments array */ -- (RKParamsAttachment *)setData:(NSData*)data MIMEType:(NSString *)MIMEType forParam:(NSString *)param; +- (RKParamsAttachment *)setData:(NSData *)data MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param DEPRECATED_ATTRIBUTE; /** - Sets the value for a named parameter to a data object with the specified MIME Type and attachment file name - - @deprecated Set the MIMEType and fileName on the returned RKParamsAttachment instead + 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 + @param param Key name of the attachment to add + @return the new RKParamsAttachment that was added to the attachments array */ -- (RKParamsAttachment *)setData:(NSData*)data MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param DEPRECATED_ATTRIBUTE; +- (RKParamsAttachment *)setFile:(NSString *)filePath MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param DEPRECATED_ATTRIBUTE; /** - 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 - - @deprecated Set the MIMEType and fileName on the returned RKParamsAttachment instead + 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 */ -- (RKParamsAttachment *)setFile:(NSString*)filePath MIMEType:(NSString *)MIMEType fileName:(NSString *)fileName forParam:(NSString *)param DEPRECATED_ATTRIBUTE; +- (NSDictionary *)dictionaryOfPlainTextParams; + + +///----------------------------------------------------------------------------- +/// @name Resetting and checking states +///----------------------------------------------------------------------------- /** - Resets the state of the RKParams stream + Resets the state of the RKParams stream. */ - (void)reset; /** - Return a composite MD5 checksum value for all attachments + Return a composite MD5 checksum value for all attachments. */ - (NSString *)MD5; diff --git a/Code/Network/RKParams.m b/Code/Network/RKParams.m index 2f6ffe3c89..8b90fc8665 100644 --- a/Code/Network/RKParams.m +++ b/Code/Network/RKParams.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/3/09. -// Copyright 2009 RestKit -// +// 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 "RKParams.h" #import "RKLog.h" -#import "NSString+MD5.h" +#import "NSString+RKAdditions.h" // Need for iOS 5 UIDevice workaround #if TARGET_OS_IPHONE @@ -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 435c8684f6..675c1a02cc 100644 --- a/Code/Network/RKParamsAttachment.h +++ b/Code/Network/RKParamsAttachment.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/6/09. -// Copyright 2009 Two Toasters -// +// 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,37 +21,82 @@ #import /** - 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 + 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 *_MIMEType; - - @private - NSString *_filePath; - NSData *_body; - NSInputStream *_bodyStream; - NSData *_MIMEHeader; - NSUInteger _MIMEHeaderLength; - NSUInteger _bodyLength; - NSUInteger _length; - NSUInteger _delivered; - id _value; + NSString *_name; + NSString *_fileName; + NSString *_MIMEType; + + @private + NSString *_filePath; + NSData *_body; + NSInputStream *_bodyStream; + NSData *_MIMEHeader; + NSUInteger _MIMEHeaderLength; + NSUInteger _bodyLength; + NSUInteger _length; + NSUInteger _delivered; + id _value; } + +///----------------------------------------------------------------------------- +/// @name Creating an Attachment +///----------------------------------------------------------------------------- + /** - The parameter name of this attachment in the multi-part document + 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. + */ +- (id)initWithName:(NSString *)name value:(id)value; + +/** + 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. + */ +- (id)initWithName:(NSString *)name data:(NSData *)data; + +/** + 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. + @return An initialized attachment with the name and the contents of the file at + the path given. + */ +- (id)initWithName:(NSString *)name file:(NSString *)filePath; + + +///----------------------------------------------------------------------------- +/// @name Working with the Attachment +///----------------------------------------------------------------------------- + +/** + The parameter name of this attachment in the multi-part document. */ @property (nonatomic, retain) NSString *name; /** 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. - - Defaults to nil + + **Default**: nil */ @property (nonatomic, retain) NSString *MIMEType; @@ -67,49 +112,54 @@ /** The name of the attached file in the MIME stream - Defaults to the name of the file attached or nil if there is not one. - */ -@property (nonatomic, retain) NSString *fileName; - -// this value is set iff the object is init through initWithName:value -@property (nonatomic, retain) id value; - -/** - Initialize a new attachment with a given parameter name and a value - */ -- (id)initWithName:(NSString *)name value:(id)value; -/** - Initialize a new attachment with a given parameter name and the data stored in an NSData object + **Default**: The name of the file attached or nil if there is not one. */ -- (id)initWithName:(NSString *)name data:(NSData *)data; +@property (nonatomic, retain) NSString *fileName; /** - Initialize a new attachment with a given parameter name and the data stored on disk at the given file path + The value that is set when initialized through initWithName:value: */ -- (id)initWithName:(NSString *)name file:(NSString *)filePath; +@property (nonatomic, retain) id value; /** - Open the attachment stream to begin reading. This will generate a MIME header and prepare the - attachment for writing to an RKParams stream + Open the attachment stream to begin reading. This will generate a MIME header + and prepare the attachment for writing to an RKParams stream. */ - (void)open; /** - The length of the entire attachment (including the MIME Header and the body) + 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; /** - Read the attachment body in a streaming fashion + 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. */ -- (NSUInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len; +- (NSString *)MD5; + + +///----------------------------------------------------------------------------- +/// @name Input streaming +///----------------------------------------------------------------------------- /** - 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. + 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. */ -- (NSString *)MD5; +- (NSUInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len; @end diff --git a/Code/Network/RKParamsAttachment.m b/Code/Network/RKParamsAttachment.m index bcf2be57a4..e62413f623 100644 --- a/Code/Network/RKParamsAttachment.m +++ b/Code/Network/RKParamsAttachment.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/6/09. -// Copyright 2009 Two Toasters -// +// 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,9 +20,9 @@ #import "RKParamsAttachment.h" #import "RKLog.h" -#import "NSData+MD5.h" +#import "NSData+RKAdditions.h" #import "FileMD5Hash.h" -#import "NSString+RestKit.h" +#import "NSString+RKAdditions.h" // Set Logging Component #undef RKLogComponent @@ -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,59 +159,58 @@ - (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; } -// NOTE: Cannot handle MD5 for files. We don't want to read the contents into memory - (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 7f40962432..390d71a812 100755 --- a/Code/Network/RKReachabilityObserver.h +++ b/Code/Network/RKReachabilityObserver.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/14/10. -// Copyright 2010 RestKit -// +// 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,139 +21,231 @@ #import #import -/// Posted when the network state has changed -extern NSString *const RKReachabilityDidChangeNotification; +///----------------------------------------------------------------------------- +/// @name Constants +///----------------------------------------------------------------------------- -/// User Info key for accessing the SCNetworkReachabilityFlags from a RKReachabilityDidChangeNotification -extern NSString *const RKReachabilityFlagsUserInfoKey; +/** + Posted when the network state has changed + */ +extern NSString * const RKReachabilityDidChangeNotification; + +/** + User Info key for accessing the SCNetworkReachabilityFlags from a + RKReachabilityDidChangeNotification + */ +extern NSString * const RKReachabilityFlagsUserInfoKey; -/// Posted when network state has been initially determined -extern NSString *const RKReachabilityWasDeterminedNotification; +/** + Posted when network state has been initially determined + */ +extern NSString * const RKReachabilityWasDeterminedNotification; typedef enum { - RKReachabilityIndeterminate, // Network reachability not yet known - RKReachabilityNotReachable, // Network is not reachable - RKReachabilityReachableViaWiFi, // Network is reachable via a WiFi connection - RKReachabilityReachableViaWWAN // Network is reachable via a "wireless wide area network" (WWAN). i.e. GPRS, Edge, etc. + /** + Network reachability not yet known + */ + RKReachabilityIndeterminate, + /** + Network is not reachable + */ + RKReachabilityNotReachable, + /** + Network is reachable via a WiFi connection + */ + RKReachabilityReachableViaWiFi, + /** + Network is reachable via a "wireless wide area network" (WWAN). i.e. GPRS, + Edge, 3G, etc. + */ + 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) + 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; + NSString *_host; + SCNetworkReachabilityRef _reachabilityRef; + BOOL _reachabilityDetermined; BOOL _monitoringLocalWiFi; - SCNetworkReachabilityFlags _reachabilityFlags; + SCNetworkReachabilityFlags _reachabilityFlags; } +///----------------------------------------------------------------------------- +/// @name Creating a Reachability Observer +///----------------------------------------------------------------------------- + /** - The remote hostname or IP address being observed for reachability. + 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. + + 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. + @return A reachability observer targeting the given hostname/IP address or nil + if it could not be observed. */ -@property (nonatomic, readonly) NSString *host; ++ (RKReachabilityObserver *)reachabilityObserverForHost:(NSString *)hostNameOrIPAddress; /** - Returns YES if reachability has been determined - - 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 + 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. */ -@property (nonatomic, readonly, getter=isReachabilityDetermined) BOOL reachabilityDetermined; ++ (RKReachabilityObserver *)reachabilityObserverForInternet; /** - Returns YES if the reachability observer is monitoring the local WiFi interface - - When the local WiFi interface is being monitored only three reachability states are possible: - RKReachabilityIndeterminate, RKReachabilityNotReachable, or RKReachabilityReachableViaWiFi. - - If the device has connectivity through a WWAN connection only it will consider the network - not reachable. - - @see reachabilityObserverForLocalWifi - */ -@property (nonatomic, readonly, getter=isMonitoringLocalWiFi) BOOL monitoringLocalWiFi; + 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. + */ ++ (RKReachabilityObserver *)reachabilityObserverForLocalWifi; /** - Returns the current network status as determined by examining the state of the currently - cached reachabilityFlags + 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. */ -@property (nonatomic, readonly) RKReachabilityNetworkStatus networkStatus; ++ (RKReachabilityObserver *)reachabilityObserverForAddress:(const struct sockaddr *)address; /** - Returns 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 - */ -@property (nonatomic, readonly) SCNetworkReachabilityFlags reachabilityFlags; + Creates and returns a RKReachabilityObserver instance observing reachability + 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. + */ ++ (RKReachabilityObserver *)reachabilityObserverForInternetAddress:(in_addr_t)internetAddress; /** - 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. - + 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. + 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. - + 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 - @return A reachability observer targeting the given hostname/IP address or nil if it could not - be observed. + @param hostNameOrIPAddress An NSString containing a hostname or IP address to + be observed. + @return A reachability observer targeting the given hostname/IP address or nil + if it could not be observed. */ -+ (RKReachabilityObserver *)reachabilityObserverForHost:(NSString *)hostNameOrIPAddress; +- (id)initWithHost:(NSString *)hostNameOrIPAddress; /** - 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. + 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. */ -+ (RKReachabilityObserver *)reachabilityObserverForInternet; +- (id)initWithAddress:(const struct sockaddr *)address; + + +///----------------------------------------------------------------------------- +/// @name Determining the Host +///----------------------------------------------------------------------------- + +/** + The remote hostname or IP address being observed for reachability. + */ +@property (nonatomic, readonly) NSString *host; + + +///----------------------------------------------------------------------------- +/// @name Managing Reachability States +///----------------------------------------------------------------------------- /** - 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. + 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 */ -+ (RKReachabilityObserver *)reachabilityObserverForLocalWifi; +@property (nonatomic, readonly, getter=isReachabilityDetermined) BOOL reachabilityDetermined; -+ (RKReachabilityObserver *)reachabilityObserverForAddress:(const struct sockaddr *)address; -+ (RKReachabilityObserver *)reachabilityObserverForInternetAddress:(in_addr_t)internetAddress; -- (id)initWithHost:(NSString *)hostNameOrIPAddress; -- (id)initWithAddress:(const struct sockaddr *)address; +/** + 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; + + +/** + 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. + */ +@property (nonatomic, readonly) SCNetworkReachabilityFlags reachabilityFlags; /** 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 @@ -162,55 +254,64 @@ typedef enum { */ - (BOOL)getFlags; -//////////////////////////////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- /// @name Reachability Introspection +///----------------------------------------------------------------------------- /** Returns YES when the Internet is reachable (via WiFi or WWAN) - - @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isNetworkReachable; /** Returns YES when we the network is reachable via WWAN - - @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isReachableViaWWAN; /** Returns YES when we the network is reachable via WiFi - - @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isReachableViaWiFi; /** 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 + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isConnectionRequired; /** Returns YES if a dynamic, on-demand connection is available - - @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isConnectionOnDemand; /** Returns YES if user intervention is required to initiate a connection - - @exception NSInternalInconsistencyException Raises an NSInternalInconsistencyException if called before reachability is determined + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if called before reachability is determined */ - (BOOL)isInterventionRequired; /** Returns a string representation of the currently cached reachabilityFlags for inspection - - @return A string containing single character representations of the bits in an SCNetworkReachabilityFlags + + @return A string containing single character representations of the bits in an + SCNetworkReachabilityFlags */ - (NSString *)reachabilityFlagsDescription; diff --git a/Code/Network/RKReachabilityObserver.m b/Code/Network/RKReachabilityObserver.m index c9ea823e53..b2956f57f2 100755 --- a/Code/Network/RKReachabilityObserver.m +++ b/Code/Network/RKReachabilityObserver.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/14/10. -// Copyright 2010 RestKit -// +// 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 92c936b856..b81ac5d8a2 100644 --- a/Code/Network/RKRequest.h +++ b/Code/Network/RKRequest.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 7/27/09. -// Copyright 2009 Two Toasters -// +// 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,258 +29,492 @@ @class RKRequestCache; /** - * HTTP methods for requests + HTTP methods for requests */ typedef enum RKRequestMethod { - RKRequestMethodGET = 0, + RKRequestMethodInvalid = -1, + RKRequestMethodGET, RKRequestMethodPOST, RKRequestMethodPUT, RKRequestMethodDELETE, RKRequestMethodHEAD } RKRequestMethod; +NSString *RKRequestMethodNameFromType(RKRequestMethod); +RKRequestMethod RKRequestMethodTypeFromName(NSString *); + /** - * Cache policy for determining how to use RKCache + Cache policy for determining how to use RKCache */ typedef enum { - // Never use the cache + /** + Never use the cache + */ RKRequestCachePolicyNone = 0, - - // Load from the cache when we are offline + /** + Load from the cache when we are offline + */ RKRequestCachePolicyLoadIfOffline = 1 << 0, - - // Load from the cache if we encounter an error + /** + Load from the cache if we encounter an error + */ RKRequestCachePolicyLoadOnError = 1 << 1, - - // Load from the cache if we have data stored and the server returns a 304 (not modified) response + /** + Load from the cache if we have data stored and the server returns a 304 + (not modified) response + */ RKRequestCachePolicyEtag = 1 << 2, - - // Load from the cache if we have data stored + /** + Load from the cache if we have data stored + */ RKRequestCachePolicyEnabled = 1 << 3, - - // Load from the cache if we are within the timeout window + /** + Load from the cache if we are within the timeout window + */ RKRequestCachePolicyTimeout = 1 << 4, - + /** + The default cache policy is etag and timeout support + */ RKRequestCachePolicyDefault = RKRequestCachePolicyEtag | RKRequestCachePolicyTimeout } RKRequestCachePolicy; + +#if TARGET_OS_IPHONE /** - * Background Request Policy - * - * On iOS 4.x and higher, UIKit provides - * support for continueing activities for a limited amount - * of time in the background. RestKit provides simple - * support for continuing a request when in the background. + 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. */ -#if TARGET_OS_IPHONE typedef enum RKRequestBackgroundPolicy { - RKRequestBackgroundPolicyNone = 0, // Take no action with regards to backgrounding - RKRequestBackgroundPolicyCancel, // Cancel the request on transition to the background - RKRequestBackgroundPolicyContinue, // Continue the request in the background until time expires - RKRequestBackgroundPolicyRequeue // Stop the request and place it back on the queue. It will fire when the app reopens + /** + Take no action with regards to backgrounding + */ + RKRequestBackgroundPolicyNone = 0, + /** + Cancel the request on transition to the background + */ + RKRequestBackgroundPolicyCancel, + /** + Continue the request in the background until time expires + */ + RKRequestBackgroundPolicyContinue, + /** + Stop the request and place it back on the queue. It will fire when the app + reopens. + */ + RKRequestBackgroundPolicyRequeue } RKRequestBackgroundPolicy; #endif +/** + Authentication type for the request + + Based on the authentication type that is selected, authentication functionality + is triggered and other options may be required. + */ typedef enum { - RKRequestAuthenticationTypeNone = 0, // Disable the use of authentication - RKRequestAuthenticationTypeHTTP, // Use NSURLConnection's HTTP AUTH auto-negotiation - RKRequestAuthenticationTypeHTTPBasic, // Force the use of HTTP Basic authentication. This will supress AUTH challenges - RKRequestAuthenticationTypeOAuth1, // Enable the use of OAuth 1.0 authentication - RKRequestAuthenticationTypeOAuth2 // Enable the use of OAuth 2.0 authentication + /** + Disable the use of authentication + */ + RKRequestAuthenticationTypeNone = 0, + /** + Use NSURLConnection's HTTP AUTH auto-negotiation + */ + 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. + */ + 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 } RKRequestAuthenticationType; -@class RKResponse, RKRequestQueue, RKReachabilityObserver; -@protocol RKRequestDelegate; +@class RKRequest, RKResponse, RKRequestQueue, RKReachabilityObserver; +@protocol RKRequestDelegate, RKConfigurationDelegate; + +///----------------------------------------------------------------------------- +/// @name Block Declarations +///----------------------------------------------------------------------------- +typedef void(^RKRequestDidLoadResponseBlock)(RKResponse *response); +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; - 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; - - #if TARGET_OS_IPHONE + RKRequestCachePolicy _cachePolicy; + + RKRequestDidLoadResponseBlock _onDidLoadResponse; + RKRequestDidFailLoadWithErrorBlock _onDidFailLoadWithError; + +#if TARGET_OS_IPHONE RKRequestBackgroundPolicy _backgroundPolicy; UIBackgroundTaskIdentifier _backgroundTaskIdentifier; - #endif +#endif } +///----------------------------------------------------------------------------- +/// @name Creating a Request +///----------------------------------------------------------------------------- + /** - * The URL this request is loading + 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. */ -@property(nonatomic, retain) NSURL *URL; ++ (RKRequest *)requestWithURL:(NSURL *)URL; /** - * The resourcePath portion of this loader's URL + Initializes a RKRequest object to load from a provided URL + + @param URL The remote URL to load + @return An RKRequest object initialized with URL. + */ +- (id)initWithURL:(NSURL *)URL; + +/** + Creates and returns a RKRequest object initialized to load content from a + provided URL with a specified delegate. + + @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. + */ ++ (RKRequest *)requestWithURL:(NSURL *)URL delegate:(id)delegate DEPRECATED_ATTRIBUTE; + +/** + Initializes a RKRequest object to load from a provided URL + + @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. + */ +- (id)initWithURL:(NSURL *)URL delegate:(id)delegate DEPRECATED_ATTRIBUTE; + + +///----------------------------------------------------------------------------- +/// @name Setting Properties +///----------------------------------------------------------------------------- + +/** + The URL this request is loading + */ +@property (nonatomic, retain) NSURL *URL; + +/** + The resourcePath portion of the request's URL */ @property (nonatomic, retain) NSString *resourcePath; /** - * The HTTP verb the request is sent via - * - * @default RKRequestMethodGET + 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, assign) RKRequestMethod method; +@property (nonatomic, retain, readonly) RKResponse *response; /** - * A serializable collection of parameters sent as the HTTP Body of the request + A serializable collection of parameters sent as the HTTP body of the request */ -@property(nonatomic, retain) NSObject *params; +@property (nonatomic, retain) NSObject *params; /** - * The delegate to inform when the request is completed - * - * If the object implements the RKRequestDelegate protocol, - * it will receive request lifecycle event messages. + A dictionary of additional HTTP Headers to send with the request */ -@property(nonatomic, assign) NSObject *delegate; +@property (nonatomic, retain) NSDictionary *additionalHTTPHeaders; /** - * A Dictionary of additional HTTP Headers to send with the request + The run loop mode under which the underlying NSURLConnection is performed + + *Default*: NSRunLoopCommonModes */ -@property(nonatomic, retain) NSDictionary *additionalHTTPHeaders; +@property (nonatomic, copy) NSString *runLoopMode; /** * An opaque pointer to associate user defined data with the request. */ -@property(nonatomic, retain) id userData; +@property (nonatomic, retain) id userData; + +/** + The underlying NSMutableURLRequest sent for this request + */ +@property (nonatomic, readonly) NSMutableURLRequest *URLRequest; /** - * The underlying NSMutableURLRequest sent for this request + 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 */ -@property(nonatomic, readonly) NSMutableURLRequest *URLRequest; +@property (nonatomic, assign) NSStringEncoding defaultHTTPEncoding; + +///----------------------------------------------------------------------------- +/// @name Working with the HTTP Body +///----------------------------------------------------------------------------- /** - * The HTTP method as a string used for this request + 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. */ -@property(nonatomic, readonly) NSString *HTTPMethod; +- (void)setBody:(NSDictionary *)body forMIMEType:(NSString *)MIMEType; /** - The request queue that this request belongs to + The HTTP body as a NSData used for this request */ -@property (nonatomic, assign) RKRequestQueue *queue; +@property (nonatomic, retain) NSData *HTTPBody; /** - * The timeout interval within which the request should be cancelled - * if no data has been received - * - * @default 120.0 + The HTTP body as a string used for this request */ -@property (nonatomic, assign) NSTimeInterval timeoutInterval; +@property (nonatomic, retain) NSString *HTTPBodyString; + + +///----------------------------------------------------------------------------- +/// @name Delegates +///----------------------------------------------------------------------------- + +/** + The delegate to inform when the request is completed + + If the object implements the RKRequestDelegate protocol, it will receive + request lifecycle event messages. + */ +@property (nonatomic, assign) id delegate; /** - * The policy to take on transition to the background (iOS 4.x and higher only) - * - * Default: RKRequestBackgroundPolicyCancel + 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 */ +@property (nonatomic, assign) id configurationDelegate; + + +///----------------------------------------------------------------------------- +/// @name Handling Blocks +///----------------------------------------------------------------------------- + +/** + 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 -@property(nonatomic, assign) RKRequestBackgroundPolicy backgroundPolicy; -@property(nonatomic, readonly) UIBackgroundTaskIdentifier backgroundTaskIdentifier; -#endif +///----------------------------------------------------------------------------- +/// @name Background Tasks +///----------------------------------------------------------------------------- /** - The reachability observer to consult for network status. Used for performing - offline cache loads. - - Generally configured by the RKClient instance that minted this request + The policy to take on transition to the background (iOS 4.x and higher only) + + **Default:** RKRequestBackgroundPolicyCancel */ -@property (nonatomic, assign) RKReachabilityObserver *reachabilityObserver; +@property (nonatomic, assign) RKRequestBackgroundPolicy backgroundPolicy; -///////////////////////////////////////////////////////////////////////// +/** + Returns the identifier of the task that has been sent to the background. + */ +@property (nonatomic, readonly) UIBackgroundTaskIdentifier backgroundTaskIdentifier; +#endif + + +///----------------------------------------------------------------------------- /// @name Authentication -///////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- /** The type of authentication to use for this request. - - When configured to RKRequestAuthenticationTypeHTTPBasic, RestKit will add - an Authorization header establishing login via HTTP Basic. This is an optimization - that skips the challenge portion of the request. - + + This must be assigned one of the following: + + - `RKRequestAuthenticationTypeNone`: Disable the use of authentication + - `RKRequestAuthenticationTypeHTTP`: Use NSURLConnection's HTTP AUTH + auto-negotiation + - `RKRequestAuthenticationTypeHTTPBasic`: 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. + - `RKRequestAuthenticationTypeOAuth1`: Enable the use of OAuth 1.0 + authentication. OAuth1ConsumerKey, OAuth1ConsumerSecret, OAuth1AccessToken, + and OAuth1AccessTokenSecret must be set. + - `RKRequestAuthenticationTypeOAuth2`: Enable the use of OAuth 2.0 + authentication. OAuth2AccessToken must be set. + **Default**: RKRequestAuthenticationTypeNone - - @see RKRequestAuthenticationType */ @property (nonatomic, assign) RKRequestAuthenticationType authenticationType; /** - The username to use for an HTTP Authentication + 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; +@property (nonatomic, retain) NSString *username; /** - The password to use for an HTTP Authentication + 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; +@property (nonatomic, retain) NSString *password; -/*** @name OAuth Secrets */ + +///----------------------------------------------------------------------------- +/// @name OAuth1 Secrets +///----------------------------------------------------------------------------- /** The OAuth 1.0 consumer key + + Used to build an Authorization header when authenticationType is + RKRequestAuthenticationTypeOAuth1 + + @see authenticationType */ -@property(nonatomic,retain) NSString *OAuth1ConsumerKey; +@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; +@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; +@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; +@property (nonatomic, retain) NSString *OAuth1AccessTokenSecret; + -/*** @name OAuth2 Secrets */ +///----------------------------------------------------------------------------- +/// @name OAuth2 Secrets +///----------------------------------------------------------------------------- /** The OAuth 2.0 access token + + Used to build an Authorization header when authenticationType is + RKRequestAuthenticationTypeOAuth2 + + @see authenticationType */ -@property(nonatomic,retain) NSString *OAuth2AccessToken; +@property (nonatomic, retain) NSString *OAuth2AccessToken; + /** - The OAuth 2.0 refresh token. Used to retrieve a new access token before expiration + 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; +@property (nonatomic, retain) NSString *OAuth2RefreshToken; -///////////////////////////////////////////////////////////////////////// -/// @name Cacheing -///////////////////////////////////////////////////////////////////////// + +///----------------------------------------------------------------------------- +/// @name Caching +///----------------------------------------------------------------------------- /** - 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 (if possible) + 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). */ @property (nonatomic, readonly) NSString *cacheKey; @@ -290,209 +524,355 @@ typedef enum { @property (nonatomic, assign) RKRequestCachePolicy cachePolicy; /** - The request cache to store and load responses for this request - + 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 + encountered. + - `RKRequestCachePolicyEtag`: Load from the cache if there is data stored and + the server returns a 304 (Not Modified) response. + - `RKRequestCachePolicyEnabled`: Load from the cache whenever data has been + stored. + - `RKRequestCachePolicyTimeout`: Load from the cache if the + cacheTimeoutInterval is reached before the server responds. */ @property (nonatomic, retain) RKRequestCache *cache; /** Returns YES if the request is cacheable - - All requets are considered cacheable unless: - 1) The method is DELETE - 2) The request body is a stream (i.e. using RKParams) + + Only GET requests are considered cacheable (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html). */ - (BOOL)isCacheable; /** - * The HTTP body as a NSData used for this request - */ -@property (nonatomic, retain) NSData *HTTPBody; + The timeout interval within which the request should not be sent and the cached + response should be used. Used if the cache policy includes + RKRequestCachePolicyTimeout. + */ +@property (nonatomic, assign) NSTimeInterval cacheTimeoutInterval; + + +///----------------------------------------------------------------------------- +/// @name Handling SSL Validation +///----------------------------------------------------------------------------- /** - * The HTTP body as a string used for this request + 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. */ -@property (nonatomic, retain) NSString *HTTPBodyString; +@property (nonatomic, assign) BOOL disableCertificateValidation; /** - * The timeout interval within which the request should not be sent - * and the cached response should be used. Used if the cache policy - * includes RKRequestCachePolicyTimeout + A set of additional certificates to be used in evaluating server SSL + certificates. */ -@property (nonatomic, assign) NSTimeInterval cacheTimeoutInterval; +@property (nonatomic, retain) NSSet *additionalRootCertificates; -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// +///----------------------------------------------------------------------------- +/// @name Sending and Managing the Request +///----------------------------------------------------------------------------- /** - * Return a REST request that is ready for dispatching + Setup the NSURLRequest. + + The request must be prepared right before dispatching. + + @return A boolean for the success of the URL preparation. */ -+ (RKRequest *)requestWithURL:(NSURL *)URL delegate:(id)delegate; +- (BOOL)prepareURLRequest; /** - * Initialize a synchronous request + The request queue that this request belongs to */ -- (id)initWithURL:(NSURL *)URL; +@property (nonatomic, assign) RKRequestQueue *queue; /** - * Initialize a REST request and prepare it for dispatching + Send the request asynchronously. It will be added to the queue and dispatched + as soon as possible. */ -- (id)initWithURL:(NSURL *)URL delegate:(id)delegate; +- (void)send; /** - * Setup the NSURLRequest. The request must be prepared right before dispatching + Immediately dispatch a request asynchronously, skipping the request queue. */ -- (BOOL)prepareURLRequest; +- (void)sendAsynchronously; /** - * Resets the state of an RKRequest so that it can be re-sent. + Send the request synchronously and return a hydrated response object. + + @return An RKResponse object with the result of the request. */ -- (void)reset; +- (RKResponse *)sendSynchronously; /** - * Send the request asynchronously. It will be added to the queue and - * dispatched as soon as possible. + Returns a Boolean value indicating whether the request has been cancelled. + + @return YES if the request was sent a cancel message, otherwise NO. */ -- (void)send; +@property(nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled; /** - * Immediately dispatch a request asynchronously, skipping the request queue + 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)sendAsynchronously; +- (void)cancel; /** - * Send the request synchronously and return a hydrated response object + The reachability observer to consult for network status. Used for performing + offline cache loads. + + Generally configured by the RKClient instance that minted this request. */ -- (RKResponse *)sendSynchronously; +@property (nonatomic, retain) RKReachabilityObserver *reachabilityObserver; + + +///----------------------------------------------------------------------------- +/// @name Resetting the State +///----------------------------------------------------------------------------- /** - * Callback performed to notify the request that the underlying NSURLConnection - * has failed with an error. + Resets the state of an RKRequest so that it can be re-sent. + */ +- (void)reset; + + +///----------------------------------------------------------------------------- +/// @name Callbacks +///----------------------------------------------------------------------------- + +/** + 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. */ - (void)didFailLoadWithError:(NSError *)error; /** - * Callback performed to notify the request that the underlying NSURLConnection - * has completed with a response. + 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; + +///----------------------------------------------------------------------------- +/// @name Timing Out the Request +///----------------------------------------------------------------------------- + /** - * 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 + 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 */ -- (void)cancel; +@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. + 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 will return an RKRequestConnectionTimeoutError via didFailLoadWithError: + Cancels request due to connection timeout exceeded. + + This method is invoked by the timeoutTimer upon its expiration and will return + an RKRequestConnectionTimeoutError via didFailLoadWithError: */ - (void)timeout; /** - * Invalidates the timeout timer. - * Called by RKResponse when the NSURLConnection begins receiving data. + Invalidates the timeout timer. + + Called by RKResponse when the NSURLConnection begins receiving data. */ - (void)invalidateTimeoutTimer; + +///----------------------------------------------------------------------------- +/// @name Determining the Request Type and State +///----------------------------------------------------------------------------- + /** - * Returns YES when this is a GET request + Returns YES when this is a GET request */ - (BOOL)isGET; /** - * Returns YES when this is a POST request + Returns YES when this is a POST request */ - (BOOL)isPOST; /** - * Returns YES when this is a PUT request + Returns YES when this is a PUT request */ - (BOOL)isPUT; /** - * Returns YES when this is a DELETE request + Returns YES when this is a DELETE request */ - (BOOL)isDELETE; /** - * Returns YES when this is a HEAD request + Returns YES when this is a HEAD request */ - (BOOL)isHEAD; /** - * Returns YES when this request is in-progress + 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 + Returns YES when this request has been completed */ -- (BOOL)isLoaded; +@property (nonatomic, assign, readonly, getter = isLoaded) BOOL loaded; /** - * Returnes YES when this request has not yet been sent + Returns YES when this request has not yet been sent */ - (BOOL)isUnsent; /** - * Returns YES when the request was sent to the specified resource path + 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. + */ +- (BOOL)wasSentToResourcePath:(NSString *)resourcePath method:(RKRequestMethod)method; + @end /** - * Lifecycle events for RKRequests + Lifecycle events for an RKRequest object */ @protocol RKRequestDelegate @optional + +///----------------------------------------------------------------------------- +/// @name Observing Request Progress +///----------------------------------------------------------------------------- + /** - * Sent when a request has finished loading + 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)request:(RKRequest *)request didLoadResponse:(RKResponse *)response; +- (void)requestWillPrepareForSend:(RKRequest *)request; /** - * Sent when a request has failed due to an error + 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 didFailLoadWithError:(NSError *)error; +- (void)request:(RKRequest *)request didReceiveResponse:(RKResponse *)response; /** - * Sent when a request has started loading + 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 + 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. + @param totalBytesWritten An integer of the total bytes that have been sent to + the remote site. + @param totalBytesExpectedToWrite An integer of the total bytes that will be + sent to the remote site. */ - (void)request:(RKRequest *)request didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite; /** - * Sent when request has received data from remote site + 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. + @param totalBytesReceived An integer of the total bytes that have been + received from the remote site. + @param totalBytesExpectedToReceive An integer of the total bytes that will be + received from the remote site. */ -- (void)request:(RKRequest*)request didReceivedData:(NSInteger)bytesReceived totalBytesReceived:(NSInteger)totalBytesReceived totalBytesExectedToReceive:(NSInteger)totalBytesExpectedToReceive; +- (void)request:(RKRequest *)request didReceiveData:(NSInteger)bytesReceived totalBytesReceived:(NSInteger)totalBytesReceived totalBytesExpectedToReceive:(NSInteger)totalBytesExpectedToReceive; + + +///----------------------------------------------------------------------------- +/// @name Handling Successful Requests +///----------------------------------------------------------------------------- /** - * Sent to the delegate when a request was cancelled + 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. + */ +- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response; + + +///----------------------------------------------------------------------------- +/// @name Handling Failed Requests +///----------------------------------------------------------------------------- + +/** + 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. + */ +- (void)request:(RKRequest *)request didFailLoadWithError:(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 - * backgrounded request expired before completion. + 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 a356ac07cd..3cd1089647 100644 --- a/Code/Network/RKRequest.m +++ b/Code/Network/RKRequest.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 7/27/09. -// Copyright 2009 RestKit -// +// 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,20 +24,74 @@ #import "RKNotifications.h" #import "Support.h" #import "RKURL.h" -#import "NSData+MD5.h" -#import "NSString+MD5.h" +#import "NSData+RKAdditions.h" +#import "NSString+RKAdditions.h" #import "RKLog.h" #import "RKRequestCache.h" #import "GCOAuth.h" -#import "NSURL+RestKit.h" +#import "NSURL+RKAdditions.h" #import "RKReachabilityObserver.h" #import "RKRequestQueue.h" #import "RKParams.h" +#import "RKParserRegistry.h" +#import "RKRequestSerialization.h" + +NSString *RKRequestMethodNameFromType(RKRequestMethod method) { + 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: + break; + } + + return nil; +} + +RKRequestMethod RKRequestMethodTypeFromName(NSString *methodName) { + if ([methodName isEqualToString:@"GET"]) { + return RKRequestMethodGET; + } else if ([methodName isEqualToString:@"POST"]) { + return RKRequestMethodPOST; + } else if ([methodName isEqualToString:@"PUT"]) { + return RKRequestMethodPUT; + } else if ([methodName isEqualToString:@"DELETE"]) { + return RKRequestMethodDELETE; + } else if ([methodName isEqualToString:@"HEAD"]) { + return RKRequestMethodHEAD; + } + + return RKRequestMethodInvalid; +} // Set Logging Component #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; @@ -63,54 +117,62 @@ @implementation RKRequest @synthesize queue = _queue; @synthesize timeoutInterval = _timeoutInterval; @synthesize reachabilityObserver = _reachabilityObserver; +@synthesize defaultHTTPEncoding = _defaultHTTPEncoding; +@synthesize configurationDelegate = _configurationDelegate; +@synthesize onDidLoadResponse; +@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 delegate:(id)delegate { - return [[[RKRequest alloc] initWithURL:URL delegate:delegate] autorelease]; ++ (RKRequest*)requestWithURL:(NSURL*)URL { + 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; - } - return self; -} - -- (id)initWithURL:(NSURL*)URL delegate:(id)delegate { - self = [self initWithURL:URL]; - if (self) { - _delegate = delegate; - } - return self; + _defaultHTTPEncoding = NSUTF8StringEncoding; + _followRedirect = YES; + } + return self; } - (id)init { self = [super init]; - if (self) { + if (self) { + self.runLoopMode = NSRunLoopCommonModes; #if TARGET_OS_IPHONE _backgroundPolicy = RKRequestBackgroundPolicyNone; - _backgroundTaskIdentifier = 0; + _backgroundTaskIdentifier = 0; BOOL backgroundOK = &UIBackgroundTaskInvalid != NULL; if (backgroundOK) { - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; + _backgroundTaskIdentifier = UIBackgroundTaskInvalid; } #endif } - + return self; } - (void)reset { - if (_isLoading) { + if (self.isLoading) { RKLogWarning(@"Request was reset while loading: %@. Canceling.", self); [self cancel]; } @@ -119,8 +181,9 @@ - (void)reset { [_URLRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData]; [_connection release]; _connection = nil; - _isLoading = NO; - _isLoaded = NO; + self.loading = NO; + self.loaded = NO; + self.cancelled = NO; } - (void)cleanupBackgroundTask { @@ -129,22 +192,31 @@ - (void)cleanupBackgroundTask { if (backgroundOK && UIBackgroundTaskInvalid == self.backgroundTaskIdentifier) { return; } - + UIApplication* app = [UIApplication sharedApplication]; if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { - [app endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; + [app endBackgroundTask:_backgroundTaskIdentifier]; + _backgroundTaskIdentifier = UIBackgroundTaskInvalid; } #endif } -- (void)dealloc { +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; self.delegate = nil; + if (_onDidLoadResponse) Block_release(_onDidLoadResponse); + if (_onDidFailLoadWithError) Block_release(_onDidFailLoadWithError); + + _delegate = nil; + _configurationDelegate = nil; + [_reachabilityObserver release]; + _reachabilityObserver = nil; [_connection cancel]; [_connection release]; _connection = nil; + [_response release]; + _response = nil; [_userData release]; _userData = nil; [_URL release]; @@ -160,7 +232,7 @@ - (void)dealloc { [_password release]; _password = nil; [_cache release]; - _cache = nil; + _cache = nil; [_OAuth1ConsumerKey release]; _OAuth1ConsumerKey = nil; [_OAuth1ConsumerSecret release]; @@ -173,13 +245,19 @@ - (void)dealloc { _OAuth2AccessToken = nil; [_OAuth2RefreshToken release]; _OAuth2RefreshToken = nil; + [onDidFailLoadWithError release]; + onDidFailLoadWithError = nil; + [onDidLoadResponse release]; + onDidLoadResponse = nil; [self invalidateTimeoutTimer]; [_timeoutTimer release]; _timeoutTimer = nil; - + [_runLoopMode release]; + _runLoopMode = nil; + // Cleanup a background task if there is any [self cleanupBackgroundTask]; - + [super dealloc]; } @@ -188,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 { @@ -217,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); @@ -249,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 - parameters = [_URL queryDictionary]; - + 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 @@ -275,16 +353,16 @@ - (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 tokenSecret:self.OAuth1AccessTokenSecret]; else echo = [GCOAuth URLRequestForPath:[_URL path] - GETParameters:[_URL queryDictionary] + GETParameters:[_URL queryParameters] scheme:[_URL scheme] - host:[_URL host] + host:[_URL hostAndPort] consumerKey:self.OAuth1ConsumerKey consumerSecret:self.OAuth1ConsumerSecret accessToken:self.OAuth1AccessToken @@ -293,16 +371,17 @@ - (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) { + RKLogTrace(@"Setting If-None-Match header to '%@'", etag); [_URLRequest setValue:etag forHTTPHeaderField:@"If-None-Match"]; } } @@ -310,56 +389,42 @@ - (void)addHeadersToRequest { // Setup the NSURLRequest. The request must be prepared right before dispatching - (BOOL)prepareURLRequest { - [_URLRequest setHTTPMethod:[self HTTPMethod]]; - [self setRequestBody]; - [self addHeadersToRequest]; - + [_URLRequest setHTTPMethod:[self HTTPMethod]]; + + if ([self.delegate respondsToSelector:@selector(requestWillPrepareForSend:)]) { + [self.delegate requestWillPrepareForSend:self]; + } + + [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]; - + return YES; } - (void)cancelAndInformDelegate:(BOOL)informDelegate { - [_connection cancel]; - [_connection release]; - _connection = nil; + self.cancelled = YES; + [_connection cancel]; + [_connection release]; + _connection = nil; [self invalidateTimeoutTimer]; - _isLoading = NO; - + self.loading = 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; - } +- (NSString *)HTTPMethod { + return RKRequestMethodNameFromType(self.method); } -// TODO: We may want to eliminate the coupling between the request queue and individual queue instances. -// We could factor the knowledge about the queue out of RKRequest entirely, but it will break behavior. +// 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 { @@ -370,20 +435,22 @@ - (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]; } @@ -410,132 +477,137 @@ - (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."); - _sentSynchronously = NO; + 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 didFinishLoad:response]; + self.loading = YES; + [self performSelector:@selector(didFinishLoad:) withObject:response afterDelay:0]; } else if ([self shouldDispatchRequest]) { [self createTimeoutTimer]; #if TARGET_OS_IPHONE // Background Request Policy support UIApplication* app = [UIApplication sharedApplication]; - if (self.backgroundPolicy == RKRequestBackgroundPolicyNone || + if (self.backgroundPolicy == RKRequestBackgroundPolicyNone || NO == [app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { // No support for background (iOS 3.x) or the policy is none -- just fire the request [self fireAsynchronousRequest]; } else if (self.backgroundPolicy == RKRequestBackgroundPolicyCancel || self.backgroundPolicy == RKRequestBackgroundPolicyRequeue) { // For cancel or requeue behaviors, we watch for background transition notifications - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(appDidEnterBackgroundNotification:) - name:UIApplicationDidEnterBackgroundNotification + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidEnterBackgroundNotification:) + name:UIApplicationDidEnterBackgroundNotification object:nil]; [self fireAsynchronousRequest]; } else if (self.backgroundPolicy == RKRequestBackgroundPolicyContinue) { RKLogInfo(@"Beginning background task to perform processing..."); - + // Fork a background task for continueing a long-running request + __block RKRequest* weakSelf = self; + __block id weakDelegate = _delegate; _backgroundTaskIdentifier = [app beginBackgroundTaskWithExpirationHandler:^{ RKLogInfo(@"Background request time expired, canceling request."); - - [self cancelAndInformDelegate:NO]; - [self cleanupBackgroundTask]; - - if ([_delegate respondsToSelector:@selector(requestDidTimeout:)]) { - [_delegate requestDidTimeout:self]; + + [weakSelf cancelAndInformDelegate:NO]; + [weakSelf cleanupBackgroundTask]; + + if ([weakDelegate respondsToSelector:@selector(requestDidTimeout:)]) { + [weakDelegate requestDidTimeout:weakSelf]; } }]; - + // Start the potentially long-running request [self fireAsynchronousRequest]; } #else [self fireAsynchronousRequest]; #endif - } else { - RKLogTrace(@"Declined to dispatch request %@: shared client reported the network is not available.", self); - - if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && - [self.cache hasResponseForRequest:self]) { + } else { + RKLogTrace(@"Declined to dispatch request %@: reachability observer reported the network is not available.", self); - _isLoading = YES; - [self didFinishLoad:[self loadResponseFromCache]]; + 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]; - NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; - [self didFailLoadWithError:error]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + errorMessage, NSLocalizedDescriptionKey, + nil]; + NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; + [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]); - [self createTimeoutTimer]; - + 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]; } - - 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]; - - if (payload == nil) { - [self didFailLoadWithError:error]; - } else { - [self didFinishLoad:response]; - } - - } else { - if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && - [self.cache hasResponseForRequest:self]) { - - 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:RKRestKitErrorDomain code:RKRequestBaseURLOfflineError userInfo:userInfo]; - [self didFailLoadWithError:error]; - response = [[[RKResponse alloc] initWithSynchronousRequest:self URLResponse:URLResponse body:payload error:error] autorelease]; - } - } - - return response; + + _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]; + + if (error.code == NSURLErrorTimedOut) { + [self timeout]; + } else if (payload == nil) { + [self didFailLoadWithError:error]; + } else { + [self didFinishLoad:response]; + } + + } else { + if (_cachePolicy & RKRequestCachePolicyLoadIfOffline && + [self.cache hasResponseForRequest:self]) { + + 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]; + } + } + + return response; } - (void)cancel { @@ -553,7 +625,7 @@ - (void)timeout { NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: errorMessage, NSLocalizedDescriptionKey, nil]; - NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestConnectionTimeoutError userInfo:userInfo]; + NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKRequestConnectionTimeoutError userInfo:userInfo]; [self didFailLoadWithError:error]; } @@ -563,22 +635,32 @@ - (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 { + self.loaded = YES; + self.loading = NO; + + if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { + [_delegate request:self didFailLoadWithError:error]; + } + + if (self.onDidFailLoadWithError) { + self.onDidFailLoadWithError(error); + } - [self didFinishLoad:[self loadResponseFromCache]]; - } else { - _isLoading = NO; - if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { - [_delegate request:self didFailLoadWithError:error]; - } - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:error forKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFailWithErrorNotification - object:self + [[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 + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFinishLoadingNotification object:self]; } - (void)updateInternalCacheDate { @@ -587,80 +669,78 @@ - (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 ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) { + [_delegate request:self didLoadResponse:self.response]; + } - if (![response wasLoadedFromCache] && [response isSuccessful] && (_cachePolicy != RKRequestCachePolicyNone)) { - [self.cache storeResponse:response forRequest:self]; - } + if (self.onDidLoadResponse) { + self.onDidLoadResponse(self.response); + } - if ([_delegate respondsToSelector:@selector(request:didLoadResponse:)]) { - [_delegate request:self didLoadResponse:finalResponse]; - } - if ([response isServiceUnavailable]) { [[NSNotificationCenter defaultCenter] postNotificationName:RKServiceDidBecomeUnavailableNotification object:self]; } - - // NOTE: This notification must be posted last as the request queue releases the request when it - // receives the notification - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:finalResponse + + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.response forKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification - object:self + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification + object:self userInfo:userInfo]; + + // NOTE: This notification must be posted last as the request queue releases the request when it + // receives the notification + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFinishLoadingNotification object:self]; } - (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 { @@ -672,14 +752,18 @@ - (void)setURL:(NSURL *)URL { - (void)setResourcePath:(NSString *)resourcePath { if ([self.URL isKindOfClass:[RKURL class]]) { - self.URL = [RKURL URLWithBaseURLString:[(RKURL*)self.URL baseURLString] resourcePath:resourcePath]; - } else { - [NSException raise:NSInvalidArgumentException format:@"Resource path can only be mutated when self.URL is an RKURL instance"]; + self.URL = [(RKURL *)self.URL URLByReplacingResourcePath:resourcePath]; + } 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 { + return (self.method == method && [self wasSentToResourcePath:resourcePath]); } - (void)appDidEnterBackgroundNotification:(NSNotification*)notification { @@ -696,19 +780,14 @@ - (void)appDidEnterBackgroundNotification:(NSNotification*)notification { } - (BOOL)isCacheable { - // DELETE is not cacheable - if (_method == RKRequestMethodDELETE) { - return NO; - } - - return YES; + return _method == RKRequestMethodGET; } - (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) { @@ -724,4 +803,31 @@ - (NSString*)cacheKey { return [compositeCacheKey MD5]; } +- (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]; + } +} + +// Deprecations ++ (RKRequest*)requestWithURL:(NSURL*)URL delegate:(id)delegate { + 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; +} + @end diff --git a/Code/Network/RKRequestCache.h b/Code/Network/RKRequestCache.h index c58c741f7e..b2bb937471 100644 --- a/Code/Network/RKRequestCache.h +++ b/Code/Network/RKRequestCache.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// +// 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,53 +20,195 @@ #import "RKRequest.h" #import "RKResponse.h" +#import "RKCache.h" /** - * Storage policy. Determines if we clear the cache out when the app is shut down. - * Cache instance needs to register for + Cache storage policy used to determines how long we keep a specific cache for. */ typedef enum { - RKRequestCacheStoragePolicyDisabled, // The cache has been disabled. Attempts to store data will silently fail - RKRequestCacheStoragePolicyForDurationOfSession, // Cache data for the length of the session. Clear cache at app exit. - RKRequestCacheStoragePolicyPermanently // Cache data permanently, until explicitly expired or flushed + /** + The cache is disabled. Attempts to store data will silently fail. + */ + RKRequestCacheStoragePolicyDisabled, + /** + Cache data for the length of the session and clear when the app exits. + */ + RKRequestCacheStoragePolicyForDurationOfSession, + /** + Cache data permanently until explicitly expired or flushed. + */ + RKRequestCacheStoragePolicyPermanently } RKRequestCacheStoragePolicy; +/** + Location of session specific cache files within the Caches path. + */ +extern NSString * const RKRequestCacheSessionCacheDirectory; + +/** + Location of permanent cache files within the Caches path. + */ +extern NSString * const RKRequestCachePermanentCacheDirectory; + +/** + @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. */ @interface RKRequestCache : NSObject { - NSString* _cachePath; RKRequestCacheStoragePolicy _storagePolicy; - NSRecursiveLock* _cacheLock; + RKCache *_cache; } -@property (nonatomic, readonly) NSString* cachePath; // Full path to the cache -@property (nonatomic, assign) RKRequestCacheStoragePolicy storagePolicy; // User can change storage policy. +///----------------------------------------------------------------------------- +/// @name Initializating the Cache +///----------------------------------------------------------------------------- -+ (NSDateFormatter*)rfc1123DateFormatter; +/** + Initializes the receiver with a cache at a given path and storage policy. -- (id)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy; + @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. + */ +- (id)initWithPath:(NSString *)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy; -- (NSString*)pathForRequest:(RKRequest*)request; +///----------------------------------------------------------------------------- +/// @name Locating the Cache +///----------------------------------------------------------------------------- -- (BOOL)hasResponseForRequest:(RKRequest*)request; +/** + Returns the full pathname to the cache. + */ +@property (nonatomic, readonly) NSString *path; + +/** + 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. + */ +- (NSString *)pathForRequest:(RKRequest *)request; + +/** + 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. + */ +- (BOOL)hasResponseForRequest:(RKRequest *)request; -- (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request; -- (RKResponse*)responseForRequest:(RKRequest*)request; +///----------------------------------------------------------------------------- +/// @name Populating the Cache +///----------------------------------------------------------------------------- + +/** + Store a request's response in the cache. -- (NSDictionary*)headersForRequest:(RKRequest*)request; + @param response The response to be stored in the cache. + @param request The request that retrieved the response. + */ +- (void)storeResponse:(RKResponse *)response forRequest:(RKRequest *)request; -- (NSString*)etagForRequest:(RKRequest*)request; +/** + Set the cache date for a request. -- (NSDate*)cacheDateForRequest:(RKRequest*)request; + @param date The date the response for a request was cached. + @param request The request to store the cache date for. + */ +- (void)setCacheDate:(NSDate *)date forRequest:(RKRequest *)request; -- (void)setCacheDate:(NSDate*)date forRequest:(RKRequest*)request; +///----------------------------------------------------------------------------- +/// @name Preparing Requests and Responses +///----------------------------------------------------------------------------- -- (void)invalidateRequest:(RKRequest*)request; +/** + 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. + */ +- (NSDictionary *)headersForRequest:(RKRequest *)request; + +/** + 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. + */ +- (NSString *)etagForRequest:(RKRequest *)request; + +/** + 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. + */ +- (NSDate *)cacheDateForRequest:(RKRequest *)request; + +/** + 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. + */ +- (RKResponse *)responseForRequest:(RKRequest *)request; + +///----------------------------------------------------------------------------- +/// @name Invalidating the Cache +///----------------------------------------------------------------------------- + +/** + The storage policy for the cache. + */ +@property (nonatomic, assign) RKRequestCacheStoragePolicy storagePolicy; + +/** + 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. + */ - (void)invalidateWithStoragePolicy:(RKRequestCacheStoragePolicy)storagePolicy; +/** + Invalidate all caches on disk. + */ - (void)invalidateAll; + +///----------------------------------------------------------------------------- +/// @name Helpers +///----------------------------------------------------------------------------- + +/** + The date formatter used to generate the cache date for the HTTP header. + */ ++ (NSDateFormatter *)rfc1123DateFormatter; + @end diff --git a/Code/Network/RKRequestCache.m b/Code/Network/RKRequestCache.m index 28e3d9ad3a..d0d376ed0e 100644 --- a/Code/Network/RKRequestCache.m +++ b/Code/Network/RKRequestCache.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// +// 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,13 +25,13 @@ #undef RKLogComponent #define RKLogComponent lcl_cRestKitNetworkCache -static NSString* sessionCacheFolder = @"SessionStore"; -static NSString* permanentCacheFolder = @"PermanentStore"; -static NSString* headersExtension = @"headers"; -static NSString* cacheDateHeaderKey = @"X-RESTKIT-CACHEDATE"; -NSString* cacheResponseCodeKey = @"X-RESTKIT-CACHED-RESPONSE-CODE"; -NSString* cacheMIMETypeKey = @"X-RESTKIT-CACHED-MIME-TYPE"; -NSString* cacheURLKey = @"X-RESTKIT-CACHED-URL"; +NSString * const RKRequestCacheSessionCacheDirectory = @"SessionStore"; +NSString * const RKRequestCachePermanentCacheDirectory = @"PermanentStore"; +NSString * const RKRequestCacheHeadersExtension = @"headers"; +NSString * const RKRequestCacheDateHeaderKey = @"X-RESTKIT-CACHEDATE"; +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,232 +40,132 @@ @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)initWithCachePath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy { +- (id)initWithPath:(NSString*)cachePath storagePolicy:(RKRequestCacheStoragePolicy)storagePolicy { self = [super init]; - if (self) { - _cachePath = [cachePath copy]; - _cacheLock = [[NSRecursiveLock alloc] init]; - - NSFileManager* fileManager = [NSFileManager defaultManager]; - NSArray* pathArray = [NSArray arrayWithObjects: - _cachePath, - [_cachePath stringByAppendingPathComponent:sessionCacheFolder], - [_cachePath stringByAppendingPathComponent:permanentCacheFolder], - nil]; - - 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 { - if (!isDirectory) { - RKLogWarning(@"Skipped creation of cache directory as non-directory file exists at path: %@", path); - } - } - } + 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 { - [_cachePath release]; - _cachePath = nil; - [_cacheLock release]; - _cacheLock = nil; - [super dealloc]; + [_cache release]; + _cache = nil; + [super dealloc]; } -- (NSString*)cachePath { - return _cachePath; +- (NSString*)path { + return _cache.cachePath; } - (NSString*)pathForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return nil; + NSString* pathForRequest = nil; + NSString* requestCacheKey = [request cacheKey]; + if (requestCacheKey) { + if (_storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { + pathForRequest = [RKRequestCacheSessionCacheDirectory stringByAppendingPathComponent:requestCacheKey]; + + } else if (_storagePolicy == RKRequestCacheStoragePolicyPermanently) { + pathForRequest = [RKRequestCachePermanentCacheDirectory stringByAppendingPathComponent:requestCacheKey]; + } + RKLogTrace(@"Found cacheKey '%@' for %@", pathForRequest, request); + } else { + RKLogTrace(@"Failed to find cacheKey for %@ due to nil cacheKey", request); } - - [_cacheLock lock]; - - NSString* pathForRequest = nil; - - if (_storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { - pathForRequest = [[_cachePath stringByAppendingPathComponent:sessionCacheFolder] - stringByAppendingPathComponent:[request cacheKey]]; - - } else if (_storagePolicy == RKRequestCacheStoragePolicyPermanently) { - pathForRequest = [[_cachePath stringByAppendingPathComponent:permanentCacheFolder] - stringByAppendingPathComponent:[request cacheKey]]; - } - - [_cacheLock unlock]; - RKLogTrace(@"Found cachePath '%@' for %@", pathForRequest, request); - - return pathForRequest; + return pathForRequest; } - (BOOL)hasResponseForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return NO; + BOOL hasEntryForRequest = NO; + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { + hasEntryForRequest = ([_cache hasEntry:cacheKey] && + [_cache hasEntry:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]); } - - [_cacheLock lock]; - - BOOL hasEntryForRequest = NO; - NSFileManager* fileManager = [NSFileManager defaultManager]; - - NSString* cachePath = [self pathForRequest:request]; - hasEntryForRequest = ([fileManager fileExistsAtPath:cachePath] && - [fileManager fileExistsAtPath: - [cachePath stringByAppendingPathExtension:headersExtension]]); - - [_cacheLock unlock]; RKLogTrace(@"Determined hasResponseForRequest: %@ => %@", request, hasEntryForRequest ? @"YES" : @"NO"); - return hasEntryForRequest; -} - -- (void)writeHeaders:(NSDictionary*)headers toCachePath:(NSString*)cachePath { - RKLogTrace(@"Writing headers to cache path: '%@'", cachePath); - BOOL success = [headers writeToFile:[cachePath - stringByAppendingPathExtension:headersExtension] - atomically:YES]; - if (success) { - RKLogTrace(@"Wrote cached response header to path '%@'", cachePath); - - } else { - RKLogError(@"Failed to write cached response headers to path '%@'", cachePath); - } + return hasEntryForRequest; } - (void)storeResponse:(RKResponse*)response forRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return; + if ([self hasResponseForRequest:request]) { + [self invalidateRequest:request]; } - - [_cacheLock lock]; - - if ([self hasResponseForRequest:request]) { - [self invalidateRequest:request]; - } - if (_storagePolicy != RKRequestCacheStoragePolicyDisabled) { - NSString* cachePath = [self pathForRequest:request]; - if (cachePath) { - NSData* body = response.body; - if (body) { - NSError* error = nil; - BOOL success = [body writeToFile:cachePath options:NSDataWritingAtomic error:&error]; - if (success) { - RKLogTrace(@"Wrote cached response body to path '%@'", cachePath); - } else { - RKLogError(@"Failed to write cached response body to path '%@': %@", cachePath, [error localizedDescription]); - } - } + 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:cacheDateHeaderKey]; + [headers setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:[NSDate date]] + forKey:RKRequestCacheDateHeaderKey]; // Cache status code [headers setObject:[NSNumber numberWithInteger:urlResponse.statusCode] - forKey:cacheResponseCodeKey]; + forKey:RKRequestCacheStatusCodeHeadersKey]; // Cache MIME Type [headers setObject:urlResponse.MIMEType - forKey:cacheMIMETypeKey]; + forKey:RKRequestCacheMIMETypeHeadersKey]; // Cache URL [headers setObject:[urlResponse.URL absoluteString] - forKey:cacheURLKey]; + forKey:RKRequestCacheURLHeadersKey]; // Save - [self writeHeaders:headers toCachePath:cachePath]; - } - - [headers release]; - } - } - - [_cacheLock unlock]; + [_cache writeDictionary:headers withCacheKey:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]; + } + [headers release]; + } + } } - (RKResponse*)responseForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return nil; + 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]; } - - [_cacheLock lock]; - - RKResponse* response = nil; - - NSString* cachePath = [self pathForRequest:request]; - if (cachePath) { - NSData* responseData = [NSData dataWithContentsOfFile:cachePath]; - - NSDictionary* responseHeaders = [NSDictionary dictionaryWithContentsOfFile: - [cachePath stringByAppendingPathExtension:headersExtension]]; - - response = [[[RKResponse alloc] initWithRequest:request body:responseData headers:responseHeaders] autorelease]; - } - - [_cacheLock unlock]; - RKLogDebug(@"Found cached RKResponse '%@' for '%@'", request, response); - return response; + RKLogDebug(@"Found cached RKResponse '%@' for '%@'", response, request); + return response; } - (NSDictionary*)headersForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return nil; - } - NSString* cachePath = [self pathForRequest:request]; - - [_cacheLock lock]; - NSDictionary* headers = nil; - if (cachePath) { - NSString* headersPath = [cachePath stringByAppendingPathExtension:headersExtension]; - headers = [NSDictionary dictionaryWithContentsOfFile:headersPath]; + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { + NSString* headersCacheKey = [cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]; + headers = [_cache dictionaryForCacheKey:headersCacheKey]; if (headers) { - RKLogDebug(@"Read cached headers '%@' from cachePath '%@' for '%@'", headers, headersPath, request); + RKLogDebug(@"Read cached headers '%@' from headersCacheKey '%@' for '%@'", headers, headersCacheKey, request); } else { - RKLogDebug(@"Read nil cached headers from cachePath '%@' for '%@'", headersPath, request); + RKLogDebug(@"Read nil cached headers from headersCacheKey '%@' for '%@'", headersCacheKey, request); } } else { - RKLogDebug(@"Unable to read cached headers for '%@': cachePath not found", request); + RKLogDebug(@"Unable to read cached headers for '%@': cacheKey not found", request); } - - [_cacheLock unlock]; return headers; } - (NSString*)etagForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return nil; - } - NSString* etag = nil; + NSString* etag = nil; NSDictionary* responseHeaders = [self headersForRequest:request]; - - [_cacheLock lock]; if (responseHeaders) { for (NSString* responseHeader in responseHeaders) { if ([[responseHeader uppercaseString] isEqualToString:[@"ETag" uppercaseString]]) { @@ -273,125 +173,72 @@ - (NSString*)etagForRequest:(RKRequest*)request { } } } - - [_cacheLock unlock]; RKLogDebug(@"Found cached ETag '%@' for '%@'", etag, request); - return etag; + return etag; } - (void)setCacheDate:(NSDate*)date forRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return; + NSString* cacheKey = [self pathForRequest:request]; + if (cacheKey) { + NSMutableDictionary* responseHeaders = [[self headersForRequest:request] mutableCopy]; + + [responseHeaders setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:date] + forKey:RKRequestCacheDateHeaderKey]; + [_cache writeDictionary:responseHeaders + withCacheKey:[cacheKey stringByAppendingPathExtension:RKRequestCacheHeadersExtension]]; + [responseHeaders release]; } - NSMutableDictionary* responseHeaders = [[self headersForRequest:request] mutableCopy]; - - [responseHeaders setObject:[[RKRequestCache rfc1123DateFormatter] stringFromDate:date] - forKey:cacheDateHeaderKey]; - [self writeHeaders:responseHeaders toCachePath:[self pathForRequest:request]]; - - [responseHeaders release]; } - (NSDate*)cacheDateForRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return nil; - } - NSDate* date = nil; + NSDate* date = nil; NSString* dateString = nil; - + NSDictionary* responseHeaders = [self headersForRequest:request]; - - [_cacheLock lock]; if (responseHeaders) { for (NSString* responseHeader in responseHeaders) { - if ([[responseHeader uppercaseString] isEqualToString:[cacheDateHeaderKey uppercaseString]]) { + if ([[responseHeader uppercaseString] isEqualToString:[RKRequestCacheDateHeaderKey uppercaseString]]) { dateString = [responseHeaders objectForKey:responseHeader]; } } } - [_cacheLock unlock]; date = [[RKRequestCache rfc1123DateFormatter] dateFromString:dateString]; - RKLogDebug(@"Found cached date '%@' for '%@'", date, request); - return date; + return date; } - (void)invalidateRequest:(RKRequest*)request { - if (! [request isCacheable]) { - return; - } - - [_cacheLock lock]; RKLogDebug(@"Invalidating cache entry for '%@'", request); - - NSString* cachePath = [self pathForRequest:request]; - if (cachePath) { - NSFileManager* fileManager = [NSFileManager defaultManager]; - [fileManager removeItemAtPath:cachePath error:NULL]; - [fileManager removeItemAtPath:[cachePath stringByAppendingPathExtension:headersExtension] - error:NULL]; - RKLogTrace(@"Removed cache entry at path '%@' for '%@'", cachePath, request); - } - - [_cacheLock unlock]; + 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 { - [_cacheLock lock]; - - if (_cachePath && storagePolicy != RKRequestCacheStoragePolicyDisabled) { - NSString* cachePath = nil; - if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { - cachePath = [_cachePath stringByAppendingPathComponent:sessionCacheFolder]; - } else { - cachePath = [_cachePath stringByAppendingPathComponent:permanentCacheFolder]; - } - - 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]; - [fileManager removeItemAtPath:[cacheEntryPath stringByAppendingPathExtension:headersExtension] - 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]; + if (storagePolicy != RKRequestCacheStoragePolicyDisabled) { + if (storagePolicy == RKRequestCacheStoragePolicyForDurationOfSession) { + [_cache invalidateSubDirectory:RKRequestCacheSessionCacheDirectory]; + } else { + [_cache invalidateSubDirectory:RKRequestCachePermanentCacheDirectory]; + } + } } - (void)invalidateAll { - [_cacheLock lock]; - RKLogInfo(@"Invalidating all cache entries..."); - [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; - [self invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - - [_cacheLock unlock]; + [_cache invalidateSubDirectory:RKRequestCacheSessionCacheDirectory]; + [_cache invalidateSubDirectory:RKRequestCachePermanentCacheDirectory]; } - (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 0bc063de78..f7308f0690 100644 --- a/Code/Network/RKRequestQueue.h +++ b/Code/Network/RKRequestQueue.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 12/1/10. -// Copyright 2010 RestKit -// +// 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,218 +24,343 @@ @protocol RKRequestQueueDelegate; /** - * A lightweight queue implementation responsible - * for dispatching and managing RKRequest objects + A lightweight queue implementation responsible for dispatching and managing + RKRequest objects. */ @interface RKRequestQueue : NSObject { NSString *_name; - NSMutableArray *_requests; + NSMutableArray *_requests; + NSMutableSet *_loadingRequests; NSObject *_delegate; - NSUInteger _loadingCount; NSUInteger _concurrentRequestsLimit; - NSUInteger _requestTimeout; - NSTimer *_queueTimer; - BOOL _suspended; + NSUInteger _requestTimeout; + NSTimer *_queueTimer; + BOOL _suspended; BOOL _showsNetworkActivityIndicatorWhenBusy; } + +///----------------------------------------------------------------------------- +/// @name Creating a Request Queue +///----------------------------------------------------------------------------- + /** - A symbolic name for the queue. Used to return existing queue references - via [RKRequestQueue queueWithName:] + Creates and returns a new request queue. + + @return An autoreleased RKRequestQueue object. */ -@property (nonatomic, retain, readonly) NSString *name; ++ (id)requestQueue; /** - * 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. + 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. */ -@property(nonatomic, assign) id delegate; ++ (id)newRequestQueueWithName:(NSString *)name; + + +///----------------------------------------------------------------------------- +/// @name Retrieving an Existing Queue +///----------------------------------------------------------------------------- /** - * The number of concurrent requests supported by this queue - * Defaults to 5 + 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. */ -@property (nonatomic) NSUInteger concurrentRequestsLimit; ++ (id)requestQueueWithName:(NSString *)name; + +///----------------------------------------------------------------------------- +/// @name Naming Queues +///----------------------------------------------------------------------------- /** - * Request timeout value used by the queue - * Defaults to 5 minutes (300 seconds) + A symbolic name for the queue. + + Used to return existing queue references via + [RKRequestQueue requestQueueWithName:] */ -@property (nonatomic, assign) NSUInteger requestTimeout; +@property (nonatomic, retain, readonly) NSString *name; /** - * Gets 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. + 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. */ -@property (nonatomic) BOOL suspended; ++ (BOOL)requestQueueExistsWithName:(NSString *)name; + + +///----------------------------------------------------------------------------- +/// @name Monitoring State Changes +///----------------------------------------------------------------------------- /** - * Returns the total number of requests that are currently loading + 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. */ -@property (nonatomic, readonly) NSUInteger loadingCount; +@property (nonatomic, assign) id delegate; + + +///----------------------------------------------------------------------------- +/// @name Managing the Queue +///----------------------------------------------------------------------------- /** - * Returns the number of requests in the queue + The number of concurrent requests supported by this queue. + + **Default**: 5 concurrent requests */ -@property (nonatomic, readonly) NSUInteger count; +@property (nonatomic) NSUInteger concurrentRequestsLimit; -#if TARGET_OS_IPHONE /** - * When YES, this queue will spin the network activity in the menu bar when it is processing - * requests - * - * *Default*: NO + Request timeout value used by the queue. + + **Default**: 5 minutes (300 seconds) */ -@property (nonatomic) BOOL showsNetworkActivityIndicatorWhenBusy; -#endif +@property (nonatomic, assign) NSUInteger requestTimeout; /** - Return the global queue - - Deprecated. All RKClient instances now own their own individual request queues. - - @see [RKClient requestQueue] + Returns the total number of requests in the queue. */ -+ (RKRequestQueue *)sharedQueue DEPRECATED_ATTRIBUTE; +@property (nonatomic, readonly) NSUInteger count; /** - Set the global queue - - Deprecated. All RKClient instances now own their own individual request queues. - - @see [RKClient requestQueue] + 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)setSharedQueue:(RKRequestQueue *)requestQueue DEPRECATED_ATTRIBUTE; +- (void)addRequest:(RKRequest *)request; /** - Returns a new auto-released request queue + Cancel a request that is in progress. + + @param request The request to be cancelled. */ -+ (id)requestQueue; +- (void)cancelRequest:(RKRequest *)request; /** - 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. + Cancel all requests with a given delegate. + + @param delegate The delegate assigned to the requests to be cancelled. */ -+ (id)newRequestQueueWithName:(NSString *)name; +- (void)cancelRequestsWithDelegate:(id)delegate; /** - Returns queue with the specified name. If no queue is found with - the name provided, a new queue will be initialized and returned. + Aborts all requests with a given delegate by nullifying the delegate + 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 cancelled + without generating any further delegate callbacks. + + @param delegate The object acting as the delegate for all enqueued requests that are to be aborted. */ -+ (id)requestQueueWithName:(NSString *)name; +- (void)abortRequestsWithDelegate:(id)delegate; /** - Returns YES when there is a queue with the given name + Cancel all active or pending requests. */ -+ (BOOL)requestQueueExistsWithName:(NSString *)name; +- (void)cancelAllRequests; /** - * Add an asynchronous request to the queue and send it as - * as soon as possible + 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. */ -- (void)addRequest:(RKRequest *)request; +- (BOOL)containsRequest:(RKRequest *)request; + + +///----------------------------------------------------------------------------- +/// @name Processing Queued Requests +///----------------------------------------------------------------------------- /** - * Cancel a request that is in progress + Start checking for and processing requests. */ -- (void)cancelRequest:(RKRequest *)request; +- (void)start; /** - * Cancel all requests with a given delegate + 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. */ -- (void)cancelRequestsWithDelegate:(NSObject *)delegate; +@property (nonatomic) BOOL suspended; /** - * Cancel all active or pending requests. + Returns the total number of requests that are currently loading. */ -- (void)cancelAllRequests; +@property (nonatomic, readonly) NSUInteger loadingCount; +#if TARGET_OS_IPHONE /** - * Start checking for and processing requests + 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 */ -- (void)start; +@property (nonatomic) BOOL showsNetworkActivityIndicatorWhenBusy; +#endif + + +///----------------------------------------------------------------------------- +/// @name Global Queues (Deprecated) +///----------------------------------------------------------------------------- /** - * Returns YES if the specified request is in this queue + Returns the global queue + + @bug **DEPRECATED** in v0.10.0: All RKClient instances now own their own + individual request queues. + + @see [RKClient requestQueue] + @return Global request queue. */ -- (BOOL)containsRequest:(RKRequest *)request; ++ (RKRequestQueue *)sharedQueue DEPRECATED_ATTRIBUTE; + +/** + Sets the global queue + + @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. + */ ++ (void)setSharedQueue:(RKRequestQueue *)requestQueue DEPRECATED_ATTRIBUTE; @end + /** - * Lifecycle events for RKRequestQueue - * + Lifecycle events for an RKRequestQueue */ @protocol RKRequestQueueDelegate @optional +///----------------------------------------------------------------------------- +/// @name Starting and Stopping the Queue +///----------------------------------------------------------------------------- + /** - * Sent when the queue has been suspended and request processing has been halted + Sent when the queue transitions from an empty state to processing requests. + + @param queue The queue that began processing requests. */ -- (void)requestQueueWasSuspended:(RKRequestQueue *)queue; +- (void)requestQueueDidBeginLoading:(RKRequestQueue *)queue; /** - * Sent when the queue has been unsuspended and request processing has resumed + Sent when queue transitions from a processing state to an empty start. + + @param queue The queue that finished processing requests. */ -- (void)requestQueueWasUnsuspended:(RKRequestQueue *)queue; +- (void)requestQueueDidFinishLoading:(RKRequestQueue *)queue; /** - * Sent when the queue transitions from an empty state to processing requests + Sent when the queue has been suspended and request processing has been halted. + + @param queue The request queue that has been suspended. */ -- (void)requestQueueDidBeginLoading:(RKRequestQueue *)queue; +- (void)requestQueueWasSuspended:(RKRequestQueue *)queue; /** - * Sent when queue transitions from a processing state to an empty start + Sent when the queue has been unsuspended and request processing has resumed. + + @param queue The request queue that has resumed processing. */ -- (void)requestQueueDidFinishLoading:(RKRequestQueue *)queue; +- (void)requestQueueWasUnsuspended:(RKRequestQueue *)queue; + + +///----------------------------------------------------------------------------- +/// @name Processing Requests +///----------------------------------------------------------------------------- /** - * Sent before queue sends a request + Sent before queue sends a request. + + @param queue The queue that will process the request. + @param request The request to be processed. */ - (void)requestQueue:(RKRequestQueue *)queue willSendRequest:(RKRequest *)request; /** - * Sent after queue has sent a request + Sent after queue has sent a request. + + @param queue The queue that processed the request. + @param request The processed request. */ - (void)requestQueue:(RKRequestQueue *)queue didSendRequest:(RKRequest *)request; /** - * Sent when queue received a response for a request + 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. */ - (void)requestQueue:(RKRequestQueue *)queue didCancelRequest:(RKRequest *)request; /** - * Sent when an attempted request fails + 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 + request to fail. */ - (void)requestQueue:(RKRequestQueue *)queue didFailRequest:(RKRequest *)request withError:(NSError *)error; @end -/** - * A category on UIApplication to allow for jointly managing of network activity indicator. - * Adopted from 'iOS Recipes' book: http://pragprog.com/book/cdirec/ios-recipes - */ - #if TARGET_OS_IPHONE +/** + 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) +/** + Returns the number of network activity requests. + */ @property (nonatomic, assign, readonly) NSInteger networkActivityCount; +/** + Push a network activity request onto the stack. + */ - (void)pushNetworkActivity; + +/** + Pop a network activity request off the stack. + */ - (void)popNetworkActivity; + +/** + Reset the network activity stack. + */ - (void)resetNetworkActivity; @end - #endif diff --git a/Code/Network/RKRequestQueue.m b/Code/Network/RKRequestQueue.m index a2ec1ee390..88eb637360 100644 --- a/Code/Network/RKRequestQueue.m +++ b/Code/Network/RKRequestQueue.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 12/1/10. -// Copyright 2010 Two Toasters -// +// 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,9 +42,6 @@ @interface RKRequestQueue () @property (nonatomic, retain, readwrite) NSString* name; - -// Declare the loading count read-write -@property (nonatomic, assign, readwrite) NSUInteger loadingCount; @end @implementation RKRequestQueue @@ -54,7 +51,6 @@ @implementation RKRequestQueue @synthesize concurrentRequestsLimit = _concurrentRequestsLimit; @synthesize requestTimeout = _requestTimeout; @synthesize suspended = _suspended; -@synthesize loadingCount = _loadingCount; #if TARGET_OS_IPHONE @synthesize showsNetworkActivityIndicatorWhenBusy = _showsNetworkActivityIndicatorWhenBusy; @@ -76,60 +72,70 @@ + (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 - for (NSValue* value in RKRequestQueueInstances) { + NSArray *requestQueueInstances = [RKRequestQueueInstances copy]; + RKRequestQueue *namedQueue = nil; + for (NSValue* value in requestQueueInstances) { RKRequestQueue* queue = (RKRequestQueue*) [value nonretainedObjectValue]; if ([queue.name isEqualToString:name]) { - return queue; + namedQueue = queue; + break; } } - - RKRequestQueue* queue = [self requestQueue]; - queue.name = name; - [RKRequestQueueInstances addObject:[NSValue valueWithNonretainedObject:queue]]; - - return queue; + [requestQueueInstances release]; + + if (namedQueue == nil) { + namedQueue = [self requestQueue]; + namedQueue.name = name; + [RKRequestQueueInstances addObject:[NSValue valueWithNonretainedObject:namedQueue]]; + } + + return namedQueue; } + (BOOL)requestQueueExistsWithName:(NSString*)name { + BOOL queueExists = NO; if (RKRequestQueueInstances) { - for (NSValue* value in RKRequestQueueInstances) { + NSArray *requestQueueInstances = [RKRequestQueueInstances copy]; + for (NSValue* value in requestQueueInstances) { RKRequestQueue* queue = (RKRequestQueue*) [value nonretainedObjectValue]; if ([queue.name isEqualToString:name]) { - return YES; + queueExists = YES; + break; } } + [requestQueueInstances release]; } - - return NO; + + return queueExists; } - + - (id)init { - if ((self = [super init])) { - _requests = [[NSMutableArray alloc] init]; - _suspended = YES; - _loadingCount = 0; - _concurrentRequestsLimit = 5; - _requestTimeout = 300; + if ((self = [super init])) { + _requests = [[NSMutableArray alloc] init]; + _loadingRequests = [[NSMutableSet alloc] init]; + _suspended = YES; + _concurrentRequestsLimit = 5; + _requestTimeout = 300; _showsNetworkActivityIndicatorWhenBusy = NO; #if TARGET_OS_IPHONE @@ -145,8 +151,8 @@ - (id)init { object:nil]; } #endif - } - return self; + } + return self; } - (void)removeFromNamedQueues { @@ -164,10 +170,12 @@ - (void)removeFromNamedQueues { - (void)dealloc { RKLogDebug(@"Queue instance is being deallocated: %@", self); [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [self removeFromNamedQueues]; [_queueTimer invalidate]; + [_loadingRequests release]; + _loadingRequests = nil; [_requests release]; _requests = nil; @@ -179,15 +187,19 @@ - (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]; } -- (void)setLoadingCount:(NSUInteger)count { - if (_loadingCount == 0 && count > 0) { - RKLogTrace(@"Loading count increasing from 0 to %ld. Firing requestQueueDidBeginLoading", (long) count); - +- (NSUInteger)loadingCount { + return [_loadingRequests count]; +} + +- (void)addLoadingRequest:(RKRequest*)request { + if (self.loadingCount == 0) { + RKLogTrace(@"Loading count increasing from 0 to 1. Firing requestQueueDidBeginLoading"); + // Transitioning from empty to processing if ([_delegate respondsToSelector:@selector(requestQueueDidBeginLoading:)]) { [_delegate requestQueueDidBeginLoading:self]; @@ -198,34 +210,43 @@ - (void)setLoadingCount:(NSUInteger)count { [[UIApplication sharedApplication] pushNetworkActivity]; } #endif - } else if (_loadingCount > 0 && count == 0) { - RKLogTrace(@"Loading count decreasing from %ld to 0. Firing requestQueueDidFinishLoading", (long) _loadingCount); - + } + @synchronized(self) { + [_loadingRequests addObject:request]; + } + RKLogTrace(@"Loading count now %ld for queue %@", (long) self.loadingCount, self); +} + +- (void)removeLoadingRequest:(RKRequest*)request { + if (self.loadingCount == 1 && [_loadingRequests containsObject:request]) { + RKLogTrace(@"Loading count decreasing from 1 to 0. Firing requestQueueDidFinishLoading"); + // Transition from processing to empty if ([_delegate respondsToSelector:@selector(requestQueueDidFinishLoading:)]) { [_delegate requestQueueDidFinishLoading:self]; } - + #if TARGET_OS_IPHONE if (self.showsNetworkActivityIndicatorWhenBusy) { [[UIApplication sharedApplication] popNetworkActivity]; } #endif } - - RKLogTrace(@"Loading count set to %ld for queue %@", (long) count, self); - _loadingCount = count; + @synchronized(self) { + [_loadingRequests removeObject:request]; + } + RKLogTrace(@"Loading count now %ld for queue %@", (long) self.loadingCount, self); } - (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 { @@ -235,7 +256,7 @@ - (RKRequest*)nextRequest { return request; } } - + return nil; } @@ -246,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) { @@ -267,9 +288,9 @@ - (void)loadNextInQueue { [_delegate requestQueue:self willSendRequest:request]; } - self.loadingCount = self.loadingCount + 1; - [request sendAsynchronously]; + [self addLoadingRequest:request]; RKLogDebug(@"Sent request %@ from queue %@. Loading count = %ld of %ld", request, self, (long) self.loadingCount, (long) _concurrentRequestsLimit); + [request sendAsynchronously]; if ([_delegate respondsToSelector:@selector(requestQueue:didSendRequest:)]) { [_delegate requestQueue:self didSendRequest:request]; @@ -279,25 +300,25 @@ - (void)loadNextInQueue { } } - if (_requests.count && !_suspended) { - [self loadNextInQueueDelayed]; - } + if (_requests.count && !_suspended) { + [self loadNextInQueueDelayed]; + } - [pool drain]; + [pool drain]; } -- (void)setSuspended:(BOOL)isSuspended { +- (void)setSuspended:(BOOL)isSuspended { if (_suspended != isSuspended) { if (isSuspended) { RKLogDebug(@"Queue %@ has been suspended", self); - + // Becoming suspended if ([_delegate respondsToSelector:@selector(requestQueueWasSuspended:)]) { [_delegate requestQueueWasSuspended:self]; } } else { RKLogDebug(@"Queue %@ has been unsuspended", self); - + // Becoming unsupended if ([_delegate respondsToSelector:@selector(requestQueueWasUnsuspended:)]) { [_delegate requestQueueWasUnsuspended:self]; @@ -305,18 +326,19 @@ - (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 { RKLogTrace(@"Request %@ added to queue %@", request, self); + NSAssert(![self containsRequest:request], @"Attempting to add the same request multiple times"); @synchronized(self) { [_requests addObject:request]; @@ -324,100 +346,115 @@ - (void)addRequest:(RKRequest*)request { } [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(requestFinishedWithNotification:) + selector:@selector(processRequestDidFinishLoadingNotification:) + name:RKRequestDidFinishLoadingNotification + object:request]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(processRequestDidLoadResponseNotification:) name:RKRequestDidLoadResponseNotification object:request]; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(requestFinishedWithNotification:) + selector:@selector(processRequestDidFailWithErrorNotification:) name:RKRequestDidFailWithErrorNotification object:request]; - - [self loadNextInQueue]; + + [self loadNextInQueue]; } -- (BOOL)removeRequest:(RKRequest*)request decrementCounter:(BOOL)decrementCounter { +- (BOOL)removeRequest:(RKRequest*)request { if ([self containsRequest:request]) { RKLogTrace(@"Removing request %@ from queue %@", request, self); @synchronized(self) { + [self removeLoadingRequest:request]; [_requests removeObject:request]; request.queue = nil; } [[NSNotificationCenter defaultCenter] removeObserver:self name:RKRequestDidLoadResponseNotification object:request]; [[NSNotificationCenter defaultCenter] removeObserver:self name:RKRequestDidFailWithErrorNotification object:request]; - - if (decrementCounter) { - NSAssert(self.loadingCount > 0, @"Attempted to decrement loading count below zero"); - self.loadingCount = self.loadingCount - 1; - RKLogTrace(@"Decremented the loading count to %ld", (long) self.loadingCount); - } + [[NSNotificationCenter defaultCenter] removeObserver:self name:RKRequestDidFinishLoadingNotification object:request]; + return YES; } - + RKLogWarning(@"Failed to remove request %@ from queue %@: it is not in the queue.", request, self); return NO; } - (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); - - // Do not decrement counter - [self removeRequest:request decrementCounter:NO]; + RKLogDebug(@"Cancelled undispatched request %@ and removed from queue %@", request, self); + + [self removeRequest:request]; request.delegate = nil; - + if ([_delegate respondsToSelector:@selector(requestQueue:didCancelRequest:)]) { [_delegate requestQueue:self didCancelRequest:request]; } - } else if ([_requests containsObject:request] && [request isLoading]) { - RKLogDebug(@"Canceled loading request %@ and removed from queue %@", request, self); - - [request cancel]; - request.delegate = nil; - + } else if ([self containsRequest:request] && [request isLoading]) { + RKLogDebug(@"Cancelled loading request %@ and removed from queue %@", request, self); + + [request cancel]; + request.delegate = nil; + if ([_delegate respondsToSelector:@selector(requestQueue:didCancelRequest:)]) { [_delegate requestQueue:self didCancelRequest:request]; } - - // Decrement the counter - [self removeRequest:request decrementCounter:YES]; - - if (loadNext) { - [self loadNextInQueue]; - } - } + + [self removeRequest:request]; + + 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 %@", self, 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]; +} + +- (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) { - [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) { + request.delegate = nil; + [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 { @@ -425,43 +462,62 @@ - (void)start { [self setSuspended:NO]; } -/** - * Invoked via observation when a request has loaded a response or failed with an error. Remove - * the completed request from the queue and continue processing - */ -- (void)requestFinishedWithNotification:(NSNotification*)notification { +- (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); + RKRequest* request = (RKRequest*)notification.object; NSDictionary* userInfo = [notification userInfo]; + + // We successfully loaded a response + RKLogDebug(@"Received response for request %@, removing from queue. (Now loading %ld of %ld)", request, (long) self.loadingCount, (long) _concurrentRequestsLimit); + + RKResponse* response = [userInfo objectForKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; + if ([_delegate respondsToSelector:@selector(requestQueue:didLoadResponse:)]) { + [_delegate requestQueue:self didLoadResponse:response]; + } + + [self removeLoadingRequest:request]; + [self loadNextInQueue]; +} + +- (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); + + RKRequest* request = (RKRequest*)notification.object; + NSDictionary* userInfo = [notification userInfo]; + + // We failed with an error + NSError* error = nil; + if (userInfo) { + error = [userInfo objectForKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; + RKLogDebug(@"Request %@ failed loading in queue %@ with error: %@.(Now loading %ld of %ld)", request, self, + [error localizedDescription], (long) self.loadingCount, (long) _concurrentRequestsLimit); + } else { + RKLogWarning(@"Received RKRequestDidFailWithErrorNotification without a userInfo, something is amiss..."); + } + + if ([_delegate respondsToSelector:@selector(requestQueue:didFailRequest:withError:)]) { + [_delegate requestQueue:self didFailRequest:request withError:error]; + } + + [self removeLoadingRequest:request]; + [self loadNextInQueue]; +} + +/* + Invoked via observation when a request has loaded a response or failed with an + error. Remove the completed request from the queue and continue processing + */ +- (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); + + RKRequest* request = (RKRequest*)notification.object; if ([self containsRequest:request]) { - // Decrement the counter - [self removeRequest:request decrementCounter:YES]; - - if ([notification.name isEqualToString:RKRequestDidLoadResponseNotification]) { - // We successfully loaded a response - RKLogDebug(@"Received response for request %@, removing from queue. (Now loading %lu of %lu)", request, (unsigned long) _loadingCount, (unsigned long) _concurrentRequestsLimit); - - RKResponse* response = [userInfo objectForKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; - if ([_delegate respondsToSelector:@selector(requestQueue:didLoadResponse:)]) { - [_delegate requestQueue:self didLoadResponse:response]; - } - } else if ([notification.name isEqualToString:RKRequestDidFailWithErrorNotification]) { - // We failed with an error - NSError* error = nil; - if (userInfo) { - error = [userInfo objectForKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; - RKLogDebug(@"Request %@ failed loading in queue %@ with error: %@.(Now loading %ld of %ld)", request, self, - [error localizedDescription], (long) _loadingCount, (long) _concurrentRequestsLimit); - } else { - RKLogWarning(@"Received RKRequestDidFailWithErrorNotification without a userInfo, something is amiss..."); - } - - if ([_delegate respondsToSelector:@selector(requestQueue:didFailRequest:withError:)]) { - [_delegate requestQueue:self didFailRequest:request withError:error]; - } - } - + [self removeRequest:request]; + // Load the next request [self loadNextInQueue]; } else { @@ -473,14 +529,14 @@ - (void)requestFinishedWithNotification:(NSNotification*)notification { - (void)willTransitionToBackground { RKLogDebug(@"App is transitioning into background, suspending queue"); - + // Suspend the queue so background requests do not trigger additional requests on state changes self.suspended = YES; } - (void)willTransitionToForeground { RKLogDebug(@"App returned from background, unsuspending queue"); - + self.suspended = NO; } diff --git a/Code/Network/RKRequestSerializable.h b/Code/Network/RKRequestSerializable.h index f7ee962388..62fbbb776f 100644 --- a/Code/Network/RKRequestSerializable.h +++ b/Code/Network/RKRequestSerializable.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/3/09. -// Copyright 2009 Two Toasters -// +// 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. @@ -18,48 +18,76 @@ // limitations under the License. // -/* - * 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. +/** + 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 + payload from disk instead of memory. + */ -@protocol RKRequestSerializable +@protocol RKRequestSerializable + +///----------------------------------------------------------------------------- +/// @name HTTP Headers +///----------------------------------------------------------------------------- /** - * The value of the Content-Type header for the HTTP Body representation of the serialization + 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; +- (NSString *)HTTPHeaderValueForContentType; @optional -/** - * NOTE: One of the following methods MUST be implemented for your serializable implementation - * to be complete. If you are allowing serialization of a small in-memory data structure, implement - * HTTPBody as it is much simpler. HTTPBodyStream provides support for streaming a large payload - * from disk instead of memory. - */ +///----------------------------------------------------------------------------- +/// @name Body Implementation +///----------------------------------------------------------------------------- /** - * An NSData representing the HTTP Body serialization of the object implementing the protocol + An NSData representing the HTTP Body serialization of the object implementing + the protocol. + + @return An NSData object respresenting the HTTP body serialization. */ -- (NSData*)HTTPBody; +- (NSData *)HTTPBody; /** - * Returns an input stream for reading the serialization as a stream. Used to provide support for - * handling large HTTP payloads. + 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; +- (NSInputStream *)HTTPBodyStream; + + +///----------------------------------------------------------------------------- +/// @name Optional HTTP Headers +///----------------------------------------------------------------------------- /** - * Returns the length of the HTTP Content-Length header + Returns the length of the HTTP Content-Length header. + + @return Unsigned integer length of the HTTP Content-Length header. */ - (NSUInteger)HTTPHeaderValueForContentLength; /** - * The value of the Content-Type header for the HTTP Body representation of the serialization - * - * @deprecated Implement HTTPHeaderValueForContentType instead + The value of the Content-Type header for the HTTP Body representation of the + serialization. + + @bug **DEPRECATED** in v0.10.0: Implement [RKRequestSerializable HTTPHeaderValueForContentType] + instead. + @return A string value of the Content-Type header for the HTTP body. */ -- (NSString*)ContentTypeHTTPHeader DEPRECATED_ATTRIBUTE; +- (NSString *)ContentTypeHTTPHeader DEPRECATED_ATTRIBUTE; @end diff --git a/Code/Network/RKRequestSerialization.h b/Code/Network/RKRequestSerialization.h index 8bce4d638c..62317f28b3 100644 --- a/Code/Network/RKRequestSerialization.h +++ b/Code/Network/RKRequestSerialization.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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,30 +21,56 @@ #import "RKRequestSerializable.h" /** - 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 - + 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 { - NSData* _data; - NSString* _MIMEType; + NSData *_data; + NSString *_MIMEType; } + +///----------------------------------------------------------------------------- +/// @name Creating a Serialization +///----------------------------------------------------------------------------- + /** - The data enclosed in this serialization + 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 + type set. */ -@property (nonatomic, readonly) NSData* data; ++ (id)serializationWithData:(NSData *)data MIMEType:(NSString *)MIMEType; + +/** + 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. + */ +- (id)initWithData:(NSData *)data MIMEType:(NSString *)MIMEType; + + +///----------------------------------------------------------------------------- +/// @name Properties +///----------------------------------------------------------------------------- /** - The MIME type of the data in this serialization + Returns the data enclosed in this serialization. */ -@property (nonatomic, readonly) NSString* MIMEType; +@property (nonatomic, readonly) NSData *data; /** - Return a new serialization enclosing an NSData object with the specified MIME Type + Returns the MIME type of the data in this serialization. */ -+ (id)serializationWithData:(NSData*)data MIMEType:(NSString*)MIMEType; +@property (nonatomic, readonly) NSString *MIMEType; @end diff --git a/Code/Network/RKRequestSerialization.m b/Code/Network/RKRequestSerialization.m index 3a9c653a32..f00c12d2ee 100644 --- a/Code/Network/RKRequestSerialization.m +++ b/Code/Network/RKRequestSerialization.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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,35 +25,35 @@ @implementation RKRequestSerialization @synthesize data = _data; @synthesize MIMEType = _MIMEType; -- (id)initWithData:(NSData*)data MIMEType:(NSString*)MIMEType { +- (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; } -+ (id)serializationWithData:(NSData*)data MIMEType:(NSString*)MIMEType { ++ (id)serializationWithData:(NSData *)data MIMEType:(NSString *)MIMEType { return [[[RKRequestSerialization alloc] initWithData:data MIMEType:MIMEType] autorelease]; } - (void)dealloc { [_data release]; [_MIMEType release]; - + [super dealloc]; } -- (NSString*)HTTPHeaderValueForContentType { +- (NSString *)HTTPHeaderValueForContentType { return self.MIMEType; } -- (NSData*)HTTPBody { +- (NSData *)HTTPBody { return self.data; } diff --git a/Code/Network/RKRequest_Internals.h b/Code/Network/RKRequest_Internals.h index 3d32506dd4..1184188d55 100644 --- a/Code/Network/RKRequest_Internals.h +++ b/Code/Network/RKRequest_Internals.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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 dc69ce4685..2ce426f39a 100644 --- a/Code/Network/RKResponse.h +++ b/Code/Network/RKResponse.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 Two Toasters -// +// 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,241 +22,374 @@ #import "RKRequest.h" /** - Models the response portion of an HTTP request/response cycle. + 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; } + +///----------------------------------------------------------------------------- +/// @name Creating a Response +///----------------------------------------------------------------------------- + /** - * The request that generated this response + 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. */ -@property(nonatomic, readonly) RKRequest* request; +- (id)initWithRequest:(RKRequest *)request; /** - * The URL the response was loaded from + 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. + @return An RKResponse object with the request, body, and header parameters set. */ -@property(nonatomic, readonly) NSURL* URL; +- (id)initWithRequest:(RKRequest *)request body:(NSData *)body headers:(NSDictionary *)headers; /** - * The MIME Type of the response body + 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. + @param body The data of the body of the response. + @param error The error returned from the NSURLConnection call, if any. + @return An RKResponse object with the results of the synchronous request + derived from the NSHTTPURLResponse and body passed. */ -@property(nonatomic, readonly) NSString* MIMEType; +- (id)initWithSynchronousRequest:(RKRequest *)request URLResponse:(NSHTTPURLResponse *)URLResponse body:(NSData *)body error:(NSError *)error; + + +///----------------------------------------------------------------------------- +/// @name Accessing the Request +///----------------------------------------------------------------------------- /** - * The status code of the HTTP response + The request that generated this response. */ -@property(nonatomic, readonly) NSInteger statusCode; +@property (nonatomic, assign, readonly) RKRequest *request; /** - * Return a dictionary of headers sent with the HTTP response + The URL the response was loaded from. */ -@property(nonatomic, readonly) NSDictionary* allHeaderFields; +@property (nonatomic, readonly) NSURL *URL; + + +///----------------------------------------------------------------------------- +/// @name Accessing the Response Components +///----------------------------------------------------------------------------- /** - * The data returned as the response body + The status code of the HTTP response. */ -@property(nonatomic, readonly) NSData* body; +@property (nonatomic, readonly) NSInteger statusCode; /** - * The error returned if the URL connection fails + Return a dictionary of headers sent with the HTTP response. */ -@property(nonatomic, readonly) NSError* failureError; +@property (nonatomic, readonly) NSDictionary *allHeaderFields; /** - * An NSArray of NSHTTPCookie objects associated with the response + An NSArray of NSHTTPCookie objects associated with the response. */ -@property(nonatomic, readonly) NSArray* cookies; +@property (nonatomic, readonly) NSArray *cookies; /** - * Initialize a new response object for a REST request + Returns the localized human readable representation of the HTTP Status Code + returned. */ -- (id)initWithRequest:(RKRequest*)request; +- (NSString *)localizedStatusCodeString; + + +///----------------------------------------------------------------------------- +/// @name Accessing Common Headers +///----------------------------------------------------------------------------- /** - * Initialize a new response object from a cached request + Returns the value of 'Content-Type' HTTP header */ -- (id)initWithRequest:(RKRequest*)request body:(NSData*)body headers:(NSDictionary*)headers; +- (NSString *)contentType; /** - * Initializes a response object from the results of a synchronous request + Returns the value of the 'Content-Length' HTTP header */ -- (id)initWithSynchronousRequest:(RKRequest*)request URLResponse:(NSHTTPURLResponse*)URLResponse body:(NSData*)body error:(NSError*)error; +- (NSString *)contentLength; /** - * Return the localized human readable representation of the HTTP Status Code returned + Returns the value of the 'Location' HTTP Header */ -- (NSString *)localizedStatusCodeString; +- (NSString *)location; + + +///----------------------------------------------------------------------------- +/// @name Reading the Body Content +///----------------------------------------------------------------------------- /** - * Return the response body as an NSString + The data returned as the response body. + */ +@property (nonatomic, readonly) NSData *body; + +/** + Returns the response body as an NSString */ - (NSString *)bodyAsString; /** - * Return the response body parsed as JSON into an object - * @deprecated in version 2.0 + Returns the response body parsed as JSON into an object + @bug **DEPRECATED** in v0.10.0 */ - (id)bodyAsJSON DEPRECATED_ATTRIBUTE; /** - * Return the response body parsed as JSON into an object + 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. */ - (id)parsedBody:(NSError **)error; + +///----------------------------------------------------------------------------- +/// @name Handling Errors +///----------------------------------------------------------------------------- + /** - * Will determine if there is an error object and use it's localized message + The error returned if the URL connection fails. + */ +@property (nonatomic, readonly) NSError *failureError; + +/** + 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 + Indicates whether the response was loaded from RKCache + + @return YES if the response was loaded from the cache */ - (BOOL)wasLoadedFromCache; + +///----------------------------------------------------------------------------- +/// @name Determining the Status Range of the Response +///----------------------------------------------------------------------------- + /** - * Indicates that the connection failed to reach the remote server. The details of the failure - * are available on the failureError reader. + 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 + 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 HTTP response code between 100 and 199 + 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 + 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 + 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 + 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 + 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; + +///----------------------------------------------------------------------------- +/// @name Determining Specific Statuses +///----------------------------------------------------------------------------- + /** - * Indicates that the response is either a server or a client error + 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. */ - (BOOL)isError; /** - * Indicates an HTTP response code of 200 + Indicates an HTTP response code of 200. + + @return YES if the response is 200 OK. */ - (BOOL)isOK; /** - * Indicates an HTTP response code of 201 + Indicates an HTTP response code of 201. + + @return YES if the response is 201 Created. */ - (BOOL)isCreated; /** - * Indicates an HTTP response code of 204 + 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 + 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 + Indicates an HTTP response code of 401. + + @return YES if the response is 401 Unauthorized. */ - (BOOL)isUnauthorized; /** - * Indicates an HTTP response code of 403 + Indicates an HTTP response code of 403. + + @return YES if the response is 403 Forbidden. */ - (BOOL)isForbidden; /** - * Indicates an HTTP response code of 404 + 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 + Indicates an HTTP response code of 409. + + @return YES if the response is 409 Conflict. */ - (BOOL)isConflict; /** - * Indicates an HTTP response code of 410 + Indicates an HTTP response code of 410. + + @return YES if the response is 410 Gone. */ - (BOOL)isGone; /** - * Indicates an HTTP response code of 422 + 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 + 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 + 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 + Indicates an HTTP response code of 503 + + @return YES if the response is 503 Service Unavailable. */ - (BOOL)isServiceUnavailable; -/** - * Returns the value of 'Content-Type' HTTP header - */ -- (NSString *)contentType; -/** - * Returns the value of the 'Content-Length' HTTP header - */ -- (NSString *)contentLength; +///----------------------------------------------------------------------------- +/// @name Accessing the Response's MIME Type and Encoding +///----------------------------------------------------------------------------- /** - * Returns the value of the 'Location' HTTP Header + The MIME Type of the response body. */ -- (NSString *)location; +@property (nonatomic, readonly) NSString *MIMEType; /** - * True when the server turned an HTML response (MIME type is text/html) + 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 (MIME type is application/xhtml+xml) + 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 (MIME type is application/xml) + 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 (MIME type is application/json) + True when the server turned an JSON response + + @return YES when the MIME type is application/json. */ - (BOOL)isJSON; @@ -266,7 +399,7 @@ - (NSString *)bodyEncodingName; /** - Return the string encoding used for the response body + Returns the string encoding used for the response body */ - (NSStringEncoding)bodyEncoding; diff --git a/Code/Network/RKResponse.m b/Code/Network/RKResponse.m index 5212311940..5a1bde7a22 100644 --- a/Code/Network/RKResponse.m +++ b/Code/Network/RKResponse.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/09. -// Copyright 2009 RestKit -// +// 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,77 +22,82 @@ #import "RKNotifications.h" #import "RKLog.h" #import "RKParserRegistry.h" -#import "RKClient.h" +#import "RKRequestCache.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent lcl_cRestKitNetwork -extern NSString* cacheResponseCodeKey; -extern NSString* cacheMIMETypeKey; -extern NSString* cacheURLKey; +#define RKResponseIgnoreDelegateIfCancelled(...) \ +if (self.request && [self.request isCancelled]) { \ +RKLogDebug(@"%s: Ignoring NSURLConnection delegate message sent after cancel.", __PRETTY_FUNCTION__); \ +return __VA_ARGS__; \ +} @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 { - [_httpURLResponse release]; - _httpURLResponse = nil; - [_body release]; - _body = nil; - [_failureError release]; - _failureError = nil; - [_responseHeaders release]; - _responseHeaders = nil; - [super dealloc]; + _request = nil; + [_httpURLResponse release]; + _httpURLResponse = nil; + [_body release]; + _body = nil; + [_failureError release]; + _failureError = nil; + [_responseHeaders release]; + _responseHeaders = nil; + [super dealloc]; } - (BOOL)hasCredentials { @@ -100,23 +105,22 @@ - (BOOL)hasCredentials { } - (BOOL)isServerTrusted:(SecTrustRef)trust { - RKClient* client = [RKClient sharedClient]; BOOL proceed = NO; - - if (client.disableCertificateValidation) { + + if (_request.disableCertificateValidation) { proceed = YES; - } else if( [client.additionalRootCertificates count] > 0 ) { - CFArrayRef rootCerts = (CFArrayRef)[client.additionalRootCertificates allObjects]; + } 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) { @@ -125,86 +129,105 @@ - (BOOL)isServerTrusted:(SecTrustRef)trust { } } } - + return proceed; } // Handle basic auth & SSL certificate validation - (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 - RKClient* client = [RKClient sharedClient]; - if (client.disableCertificateValidation || [client.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 { - [_body appendData:data]; + RKResponseIgnoreDelegateIfCancelled(); + [_body appendData:data]; [_request invalidateTimeoutTimer]; - if ([[_request delegate] respondsToSelector:@selector(request:didReceivedData:totalBytesReceived:totalBytesExectedToReceive:)]) { - [[_request delegate] request:_request didReceivedData:[data length] totalBytesReceived:[_body length] totalBytesExectedToReceive:_httpURLResponse.expectedContentLength]; + if ([[_request delegate] respondsToSelector:@selector(request:didReceiveData:totalBytesReceived:totalBytesExpectedToReceive:)]) { + [[_request delegate] request:_request didReceiveData:[data length] totalBytesReceived:[_body length] totalBytesExpectedToReceive:_httpURLResponse.expectedContentLength]; } } -- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response { +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response { + 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 { - RKLogTrace(@"Read response body: %@", [self bodyAsString]); - [_request didFinishLoad:self]; + RKResponseIgnoreDelegateIfCancelled(); + RKLogTrace(@"Read response body: %@", [self bodyAsString]); + [_request didFinishLoad:self]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + RKResponseIgnoreDelegateIfCancelled(); _failureError = [error retain]; [_request invalidateTimeoutTimer]; [_request didFailLoadWithError:_failureError]; } - (NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(NSURLRequest *)request { + RKResponseIgnoreDelegateIfCancelled(nil); RKLogWarning(@"RestKit was asked to retransmit a new body stream for a request. Possible connection error or authentication challenge?"); return nil; } @@ -216,35 +239,37 @@ - (NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(N // in connection:didReceiveResponse: to ensure that the RKRequestDelegate // callbacks get called in the correct order. - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { - - if ([[_request delegate] respondsToSelector:@selector(request:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:)]) { - [[_request delegate] request:_request didSendBodyData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite]; - } + RKResponseIgnoreDelegateIfCancelled(); + [_request invalidateTimeoutTimer]; + + 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); } - return (cfEncoding == kCFStringEncodingInvalidId) ? NSUTF8StringEncoding : CFStringConvertEncodingToNSStringEncoding(cfEncoding); + return (cfEncoding == kCFStringEncodingInvalidId) ? self.request.defaultHTTPEncoding : CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - (NSString *)bodyAsString { - return [[[NSString alloc] initWithData:self.body encoding:[self bodyEncoding]] autorelease]; + return [[[NSString alloc] initWithData:self.body encoding:[self bodyEncoding]] autorelease]; } - (id)bodyAsJSON { @@ -269,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:cacheURLKey]]; + return [NSURL URLWithString:[_responseHeaders valueForKey:RKRequestCacheURLHeadersKey]]; } - return [_httpURLResponse URL]; + return [_httpURLResponse URL]; } - (NSString*)MIMEType { if ([self wasLoadedFromCache]) { - return [_responseHeaders valueForKey:cacheMIMETypeKey]; + return [_responseHeaders valueForKey:RKRequestCacheMIMETypeHeadersKey]; } - return [_httpURLResponse MIMEType]; + return [_httpURLResponse MIMEType]; } - (NSInteger)statusCode { if ([self wasLoadedFromCache]) { - return [[_responseHeaders valueForKey:cacheResponseCodeKey] 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 { @@ -381,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 2ed78a6aa8..8fca84acf9 100644 --- a/Code/Network/RKURL.h +++ b/Code/Network/RKURL.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 10/18/10. -// Copyright 2010 Two Toasters -// +// 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,23 +19,214 @@ // /** - Extends the Cocoa NSURL base class to provide support for the concepts - of base URL and resource path that are used extensively throughout the RestKit - system. + RKURL extends the Cocoa NSURL base class to provide support for the concepts of + base URL and resource path that are used extensively throughout the RestKit + 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]; + */ +@interface RKURL : NSURL + +///----------------------------------------------------------------------------- +/// @name Creating an RKURL +///----------------------------------------------------------------------------- + +/** + 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. + */ ++ (id)URLWithBaseURL:(NSURL *)baseURL; + +/** + 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. + */ ++ (id)URLWithBaseURL:(NSURL *)baseURL resourcePath:(NSString *)resourcePath; + +/** + 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. + @return An RKURL object initialized with baseURL, resourcePath, and + queryParameters. + */ ++ (id)URLWithBaseURL:(NSURL *)baseURL resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters; + +/** + 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. + */ ++ (id)URLWithBaseURLString:(NSString *)baseURLString; + +/** + 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. + */ ++ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath; + +/** + 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. + @return An RKURL object initialized with baseURLString, resourcePath and + queryParameters. + */ ++ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters; + +/** + 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. + @return An RKURL object initialized with baseURL, resourcePath and queryParameters. */ -@interface RKURL : NSURL { - NSString* _baseURLString; - NSString* _resourcePath; - NSDictionary* _queryParams; -} +- (id)initWithBaseURL:(NSURL *)theBaseURL resourcePath:(NSString *)theResourcePath queryParameters:(NSDictionary *)theQueryParameters; -@property (nonatomic, readonly) NSString* baseURLString; -@property (nonatomic, readonly) NSString* resourcePath; -@property (nonatomic, readonly) NSDictionary* queryParams; -- (id)initWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath; -- (id)initWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath queryParams:(NSDictionary*)queryParams; -+ (RKURL*)URLWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath; -+ (RKURL*)URLWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath queryParams:(NSDictionary*)queryParams; +///----------------------------------------------------------------------------- +/// @name Accessing the URL parts +///----------------------------------------------------------------------------- + +/** + 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. + */ +@property (nonatomic, copy, readonly) NSURL *baseURL; + +/** + Returns the resource path of the receiver. + + The resource path is the path portion of the complete URL beyond that contained + in the baseURL. + */ +@property (nonatomic, copy, readonly) NSString *resourcePath; + +/** + 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. + */ +@property (nonatomic, readonly) NSDictionary *queryParameters; + + +///----------------------------------------------------------------------------- +/// @name Modifying the URL +///----------------------------------------------------------------------------- + +/** + 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. + */ +- (RKURL *)URLByAppendingResourcePath:(NSString *)theResourcePath; + +/** + 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. + @return A new RKURL that refers to a new resource at theResourcePath with a new + query component including the values from theQueryParameters. + */ +- (RKURL *)URLByAppendingResourcePath:(NSString *)theResourcePath queryParameters:(NSDictionary *)theQueryParameters; + +/** + 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 + query component including the values from theQueryParameters. + */ +- (RKURL *)URLByAppendingQueryParameters:(NSDictionary *)theQueryParameters; + +/** + 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. + */ +- (RKURL *)URLByReplacingResourcePath:(NSString *)newResourcePath; + +/** + 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" + 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 + interpolated with properties of object. + */ +- (RKURL *)URLByInterpolatingResourcePathWithObject:(id)object; @end diff --git a/Code/Network/RKURL.m b/Code/Network/RKURL.m index 5ba594f419..bcced9c11c 100644 --- a/Code/Network/RKURL.m +++ b/Code/Network/RKURL.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 10/18/10. -// Copyright 2010 Two Toasters -// +// 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,57 +20,133 @@ #import "RKURL.h" #import "RKClient.h" +#import "NSURL+RKAdditions.h" +#import "NSString+RKAdditions.h" +#import "NSDictionary+RKAdditions.h" +#import "RKLog.h" + +@interface RKURL () +@property (nonatomic, copy, readwrite) NSURL *baseURL; +@property (nonatomic, copy, readwrite) NSString *resourcePath; +@end @implementation RKURL -@synthesize baseURLString = _baseURLString; -@synthesize resourcePath = _resourcePath; -@synthesize queryParams = _queryParams; +@synthesize baseURL; +@synthesize resourcePath; -+ (RKURL*)URLWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath { - return [[[self alloc] initWithBaseURLString:baseURLString resourcePath:resourcePath] autorelease]; ++ (id)URLWithBaseURL:(NSURL *)baseURL { + return [self URLWithBaseURL:baseURL resourcePath:nil queryParameters:nil]; } -+ (RKURL*)URLWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath queryParams:(NSDictionary*)queryParams { - return [[[self alloc] initWithBaseURLString:baseURLString resourcePath:resourcePath queryParams:queryParams] autorelease]; ++ (id)URLWithBaseURL:(NSURL *)baseURL resourcePath:(NSString *)resourcePath { + return [self URLWithBaseURL:baseURL resourcePath:resourcePath queryParameters:nil]; } -- (id)initWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath { - return [self initWithBaseURLString:baseURLString resourcePath:resourcePath queryParams:nil]; ++ (id)URLWithBaseURL:(NSURL *)baseURL resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters { + return [[[self alloc] initWithBaseURL:baseURL resourcePath:resourcePath queryParameters:queryParameters] autorelease]; } -- (id)initWithBaseURLString:(NSString*)baseURLString resourcePath:(NSString*)resourcePath queryParams:(NSDictionary*)queryParams { - NSString* resourcePathWithQueryString = RKPathAppendQueryParams(resourcePath, queryParams); - NSURL *baseURL = [NSURL URLWithString:baseURLString]; - NSString* completePath = [[baseURL path] stringByAppendingPathComponent:resourcePathWithQueryString]; - // Preserve trailing slash in resourcePath - if (resourcePath && [resourcePath characterAtIndex:[resourcePath length] - 1] == '/') { - completePath = [completePath stringByAppendingString:@"/"]; ++ (id)URLWithBaseURLString:(NSString *)baseURLString { + return [self URLWithBaseURLString:baseURLString resourcePath:nil queryParameters:nil]; +} + ++ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath { + return [self URLWithBaseURLString:baseURLString resourcePath:resourcePath queryParameters:nil]; +} + ++ (id)URLWithBaseURLString:(NSString *)baseURLString resourcePath:(NSString *)resourcePath queryParameters:(NSDictionary *)queryParameters { + return [self URLWithBaseURL:[NSURL URLWithString:baseURLString] resourcePath:resourcePath queryParameters:queryParameters]; +} + +// Designated initializer. Note this diverges from NSURL due to a bug in Cocoa. We can't +// call initWithString:relativeToURL: from a subclass. +- (id)initWithBaseURL:(NSURL *)theBaseURL resourcePath:(NSString *)theResourcePath queryParameters:(NSDictionary *)theQueryParameters { + // Merge any existing query parameters with the incoming dictionary + NSDictionary *resourcePathQueryParameters = [theResourcePath queryParameters]; + 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]; + if (!completeURL) { + RKLogError(@"Failed to build RKURL by appending resourcePath and query parameters '%@' to baseURL '%@'", theResourcePath, theBaseURL); + [self release]; + return nil; + } + + self = [self initWithString:[completeURL absoluteString]]; + if (self) { + self.baseURL = theBaseURL; + self.resourcePath = theResourcePath; } - NSURL* completeURL = [NSURL URLWithString:completePath relativeToURL:baseURL]; - if (!completeURL) { - [self release]; - return nil; - } - - // You can't safely use initWithString:relativeToURL: in a NSURL subclass, see http://www.openradar.me/9729706 - self = [self initWithString:[completeURL absoluteString]]; - if (self) { - _baseURLString = [baseURLString copy]; - _resourcePath = [resourcePath copy]; - _queryParams = [queryParams retain]; - } - return self; + + return self; } - (void)dealloc { - [_baseURLString release]; - _baseURLString = nil; - [_resourcePath release]; - _resourcePath = nil; - [_queryParams release]; - _queryParams = nil; - [super dealloc]; + [baseURL release]; + baseURL = nil; + [resourcePath release]; + resourcePath = nil; + + [super dealloc]; +} + +- (NSDictionary *)queryParameters { + if (self.query) { + return [NSDictionary dictionaryWithURLEncodedString:self.query]; + } + return nil; +} + +- (RKURL *)URLByAppendingResourcePath:(NSString *)theResourcePath { + return [RKURL URLWithBaseURL:self resourcePath:theResourcePath]; +} + +- (RKURL *)URLByAppendingResourcePath:(NSString *)theResourcePath queryParameters:(NSDictionary *)theQueryParameters { + return [RKURL URLWithBaseURL:self resourcePath:theResourcePath queryParameters:theQueryParameters]; +} + +- (RKURL *)URLByAppendingQueryParameters:(NSDictionary *)theQueryParameters { + return [RKURL URLWithBaseURL:self resourcePath:nil queryParameters:theQueryParameters]; +} + +- (RKURL *)URLByReplacingResourcePath:(NSString *)newResourcePath { + return [RKURL URLWithBaseURL:self.baseURL resourcePath:newResourcePath]; +} + +- (RKURL *)URLByInterpolatingResourcePathWithObject:(id)object { + return [self URLByReplacingResourcePath:[self.resourcePath interpolateWithObject:object]]; +} + +#pragma mark - NSURL Overloads + +/* + Overload implementations from NSURL. We consider a naked string to be initialized + with a baseURL == self. Otherwise appending/replacing resourcePath will not work. + */ + ++ (id)URLWithString:(NSString *)URLString { + return [self URLWithBaseURLString:URLString]; +} + +- (id)initWithString:(NSString *)URLString { + self = [super initWithString:URLString]; + if (self) { + self.baseURL = self; + } + + return self; } @end diff --git a/Code/ObjectMapping/ObjectMapping.h b/Code/ObjectMapping/ObjectMapping.h index dda5a1fa51..293fe2e6c7 100644 --- a/Code/ObjectMapping/ObjectMapping.h +++ b/Code/ObjectMapping/ObjectMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/30/10. -// Copyright 2010 Two Toasters -// +// 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 new file mode 100644 index 0000000000..cc70f66364 --- /dev/null +++ b/Code/ObjectMapping/RKConfigurationDelegate.h @@ -0,0 +1,39 @@ +// +// RKConfigurationDelegate.h +// RestKit +// +// Created by Blake Watters on 1/7/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +@class RKRequest, RKObjectLoader; + +/** + The RKConfigurationDelegate formal protocol defines + methods enabling the centralization of RKRequest and + 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. + */ +@protocol RKConfigurationDelegate + +@optional + +/** + 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; + +@end diff --git a/Code/ObjectMapping/RKDynamicObjectMapping.h b/Code/ObjectMapping/RKDynamicObjectMapping.h index fdb2f752c2..c0c70b3cd5 100644 --- a/Code/ObjectMapping/RKDynamicObjectMapping.h +++ b/Code/ObjectMapping/RKDynamicObjectMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/11. -// Copyright 2011 RestKit -// +// 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,12 +27,12 @@ @protocol RKDynamicObjectMappingDelegate @required -- (RKObjectMapping*)objectMappingForData:(id)data; +- (RKObjectMapping *)objectMappingForData:(id)data; @end #ifdef NS_BLOCKS_AVAILABLE -typedef RKObjectMapping*(^RKDynamicObjectMappingDelegateBlock)(id); +typedef RKObjectMapping *(^RKDynamicObjectMappingDelegateBlock)(id); #endif /** @@ -40,19 +40,18 @@ typedef RKObjectMapping*(^RKDynamicObjectMappingDelegateBlock)(id); object mapping to apply at mapping time. This allows you to map very similar payloads differently depending on the type of data contained therein. */ -@interface RKDynamicObjectMapping : NSObject { +@interface RKDynamicObjectMapping : RKObjectMappingDefinition { NSMutableArray *_matchers; id _delegate; #ifdef NS_BLOCKS_AVAILABLE RKDynamicObjectMappingDelegateBlock _objectMappingForDataBlock; #endif - BOOL _forceCollectionMapping; } /** 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; @@ -65,35 +64,29 @@ typedef RKObjectMapping*(^RKDynamicObjectMappingDelegateBlock)(id); @property (nonatomic, copy) RKDynamicObjectMappingDelegateBlock objectMappingForDataBlock; #endif -/** - When YES, an NSDictionary encountered by RKObjectMapper will be treated as a collection - rather than as a single mappable entity. This is used to perform sub-keypath mapping wherein - the keys of the dictionary are part of the mappable data. - */ -@property (nonatomic, assign) BOOL forceCollectionMapping; - /** Return a new auto-released dynamic object mapping */ + (RKDynamicObjectMapping *)dynamicMapping; #if NS_BLOCKS_AVAILABLE - + /** Return a new auto-released dynamic object mapping after yielding it to the block for configuration */ -+ (RKDynamicObjectMapping *)dynamicMappingWithBlock:(void(^)(RKDynamicObjectMapping *))block; ++ (RKDynamicObjectMapping *)dynamicMappingUsingBlock:(void(^)(RKDynamicObjectMapping *dynamicMapping))block; ++ (RKDynamicObjectMapping *)dynamicMappingWithBlock:(void(^)(RKDynamicObjectMapping *dynamicMapping))block DEPRECATED_ATTRIBUTE; #endif /** 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"]; @@ -110,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 40957a03a4..1ed3d39652 100644 --- a/Code/ObjectMapping/RKDynamicObjectMapping.m +++ b/Code/ObjectMapping/RKDynamicObjectMapping.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/11. -// Copyright 2011 Two Toasters -// +// 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,67 +19,18 @@ // #import "RKDynamicObjectMapping.h" +#import "RKDynamicObjectMappingMatcher.h" #import "RKLog.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent lcl_cRestKitObjectMapping -// Implemented in RKObjectMappingOperation -BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue); - -@interface RKDynamicObjectMappingMatcher : NSObject { - NSString* _keyPath; - id _value; - RKObjectMapping* _objectMapping; -} - -@property (nonatomic, readonly) RKObjectMapping* objectMapping; - -- (id)initWithKey:(NSString*)key value:(id)value objectMapping:(RKObjectMapping*)objectMapping; -- (BOOL)isMatchForData:(id)data; -- (NSString*)matchDescription; -@end - -@implementation RKDynamicObjectMappingMatcher - -@synthesize objectMapping = _objectMapping; - -- (id)initWithKey:(NSString*)key value:(id)value objectMapping:(RKObjectMapping*)objectMapping { - self = [super init]; - if (self) { - _keyPath = [key retain]; - _value = [value retain]; - _objectMapping = [objectMapping retain]; - } - - return self; -} - -- (void)dealloc { - [_keyPath release]; - [_value release]; - [_objectMapping release]; - [super dealloc]; -} - -- (BOOL)isMatchForData:(id)data { - return RKObjectIsValueEqualToValue([data valueForKeyPath:_keyPath], _value); -} - -- (NSString*)matchDescription { - return [NSString stringWithFormat:@"%@ == %@", _keyPath, _value]; -} - -@end - -/////////////////////////////////////////////////////////////////////////////////////////////////// @implementation RKDynamicObjectMapping @synthesize delegate = _delegate; @synthesize objectMappingForDataBlock = _objectMappingForDataBlock; -@synthesize forceCollectionMapping = _forceCollectionMapping; + (RKDynamicObjectMapping*)dynamicMapping { return [[self new] autorelease]; @@ -87,12 +38,16 @@ + (RKDynamicObjectMapping*)dynamicMapping { #if NS_BLOCKS_AVAILABLE -+ (RKDynamicObjectMapping*)dynamicMappingWithBlock:(void(^)(RKDynamicObjectMapping*))block { ++ (RKDynamicObjectMapping *)dynamicMappingUsingBlock:(void(^)(RKDynamicObjectMapping *))block { RKDynamicObjectMapping* mapping = [self dynamicMapping]; block(mapping); return mapping; } ++ (RKDynamicObjectMapping*)dynamicMappingWithBlock:(void(^)(RKDynamicObjectMapping*))block { + return [self dynamicMappingUsingBlock:block]; +} + #endif - (id)init { @@ -100,7 +55,7 @@ - (id)init { if (self) { _matchers = [NSMutableArray new]; } - + return self; } @@ -119,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]) { @@ -129,7 +84,7 @@ - (RKObjectMapping*)objectMappingForDictionary:(NSDictionary*)data { return matcher.objectMapping; } } - + // Otherwise consult the delegates if (self.delegate) { mapping = [self.delegate objectMappingForData:data]; @@ -138,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/RKDynamicObjectMappingMatcher.h b/Code/ObjectMapping/RKDynamicObjectMappingMatcher.h new file mode 100644 index 0000000000..0a92b53362 --- /dev/null +++ b/Code/ObjectMapping/RKDynamicObjectMappingMatcher.h @@ -0,0 +1,30 @@ +// +// RKDynamicObjectMappingMatcher.h +// RestKit +// +// Created by Jeff Arena on 8/2/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "RKObjectMapping.h" + + +@interface RKDynamicObjectMappingMatcher : NSObject { + NSString* _keyPath; + id _value; + RKObjectMapping* _objectMapping; + NSString* _primaryKeyAttribute; + BOOL (^_isMatchForDataBlock)(id data); +} + +@property (nonatomic, readonly) RKObjectMapping* objectMapping; +@property (nonatomic, readonly) NSString* primaryKeyAttribute; + +- (id)initWithKey:(NSString*)key value:(id)value objectMapping:(RKObjectMapping*)objectMapping; +- (id)initWithKey:(NSString*)key value:(id)value primaryKeyAttribute:(NSString*)primaryKeyAttribute; +- (id)initWithPrimaryKeyAttribute:(NSString*)primaryKeyAttribute evaluationBlock:(BOOL (^)(id data))block; +- (BOOL)isMatchForData:(id)data; +- (NSString*)matchDescription; + +@end diff --git a/Code/ObjectMapping/RKDynamicObjectMappingMatcher.m b/Code/ObjectMapping/RKDynamicObjectMappingMatcher.m new file mode 100644 index 0000000000..0fcecff9a4 --- /dev/null +++ b/Code/ObjectMapping/RKDynamicObjectMappingMatcher.m @@ -0,0 +1,78 @@ +// +// RKDynamicObjectMappingMatcher.m +// RestKit +// +// Created by Jeff Arena on 8/2/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKDynamicObjectMappingMatcher.h" + + +// Implemented in RKObjectMappingOperation +BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue); + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation RKDynamicObjectMappingMatcher + +@synthesize objectMapping = _objectMapping; +@synthesize primaryKeyAttribute = _primaryKeyAttribute; + +- (id)initWithKey:(NSString*)key value:(id)value objectMapping:(RKObjectMapping*)objectMapping { + self = [super init]; + if (self) { + _keyPath = [key retain]; + _value = [value retain]; + _objectMapping = [objectMapping retain]; + } + + return self; +} + +- (id)initWithKey:(NSString*)key value:(id)value primaryKeyAttribute:(NSString*)primaryKeyAttribute { + self = [super init]; + if (self) { + _keyPath = [key retain]; + _value = [value retain]; + _primaryKeyAttribute = [primaryKeyAttribute retain]; + } + + return self; +} + +- (id)initWithPrimaryKeyAttribute:(NSString*)primaryKeyAttribute evaluationBlock:(BOOL (^)(id data))block { + self = [super init]; + if (self) { + _primaryKeyAttribute = [primaryKeyAttribute retain]; + _isMatchForDataBlock = Block_copy(block); + } + return self; +} + +- (void)dealloc { + [_keyPath release]; + [_value release]; + [_objectMapping release]; + [_primaryKeyAttribute release]; + if (_isMatchForDataBlock) { + Block_release(_isMatchForDataBlock); + } + [super dealloc]; +} + +- (BOOL)isMatchForData:(id)data { + if (_isMatchForDataBlock) { + return _isMatchForDataBlock(data); + } + return RKObjectIsValueEqualToValue([data valueForKeyPath:_keyPath], _value); +} + +- (NSString*)matchDescription { + if (_isMatchForDataBlock) { + return @"No description available. Using block to perform match."; + } + return [NSString stringWithFormat:@"%@ == %@", _keyPath, _value]; +} + +@end diff --git a/Code/ObjectMapping/RKErrorMessage.h b/Code/ObjectMapping/RKErrorMessage.h index d6a5ba2cef..5a20a993fa 100644 --- a/Code/ObjectMapping/RKErrorMessage.h +++ b/Code/ObjectMapping/RKErrorMessage.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 5/10/11. -// Copyright 2011 Two Toasters -// +// 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 f002eaa82d..6df33abada 100644 --- a/Code/ObjectMapping/RKErrorMessage.m +++ b/Code/ObjectMapping/RKErrorMessage.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 5/10/11. -// Copyright 2011 Two Toasters -// +// 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 d131691de9..7d3b5c6acf 100644 --- a/Code/ObjectMapping/RKMappingOperationQueue.h +++ b/Code/ObjectMapping/RKMappingOperationQueue.h @@ -3,7 +3,7 @@ // RestKit // // Created by Blake Watters on 9/20/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -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 5f3d47bc04..7f36a3eb0a 100644 --- a/Code/ObjectMapping/RKMappingOperationQueue.m +++ b/Code/ObjectMapping/RKMappingOperationQueue.m @@ -3,7 +3,7 @@ // RestKit // // Created by Blake Watters on 9/20/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKMappingOperationQueue.h" @@ -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 eecd364ae3..93e859ba23 100644 --- a/Code/ObjectMapping/RKObjectAttributeMapping.h +++ b/Code/ObjectMapping/RKObjectAttributeMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 RestKit -// +// 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 e2e6920dfb..d92dcee9ed 100644 --- a/Code/ObjectMapping/RKObjectAttributeMapping.m +++ b/Code/ObjectMapping/RKObjectAttributeMapping.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 RestKit -// +// 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 2b709ddcae..e80453f2b1 100644 --- a/Code/ObjectMapping/RKObjectLoader.h +++ b/Code/ObjectMapping/RKObjectLoader.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/8/09. -// Copyright 2009 Two Toasters -// +// 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,10 +21,27 @@ #import "Network.h" #import "RKObjectMapping.h" #import "RKObjectMappingResult.h" +#import "RKObjectMappingProvider.h" -@class RKObjectManager; +@class RKObjectMappingProvider; @class RKObjectLoader; +// Block Types +typedef void(^RKObjectLoaderBlock)(RKObjectLoader *loader); +typedef void(^RKObjectLoaderDidFailWithErrorBlock)(NSError *error); +typedef void(^RKObjectLoaderDidLoadObjectsBlock)(NSArray *objects); +typedef void(^RKObjectLoaderDidLoadObjectBlock)(id object); +typedef void(^RKObjectLoaderDidLoadObjectsDictionaryBlock)(NSDictionary *dictionary); + +/** + 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 + RKRequestDelegate protocol and the delegate may provide implementations of methods from RKRequestDelegate + as well. + + @see RKRequestDelegate + */ @protocol RKObjectLoaderDelegate @required @@ -32,7 +49,7 @@ /** * Sent when an object loaded failed to load the collection due to an error */ -- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error; +- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error; @optional @@ -41,63 +58,73 @@ and loaded a collection of objects. All objects mapped from the remote payload will be returned as a single array. */ -- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects; +- (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. */ -- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObject:(id)object; +- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObject:(id)object; /** When implemented, sent to the delegate when an object loader has completed successfully. The dictionary will be expressed as pairs of keyPaths and objects mapped from the payload. This method is useful when you have multiple root objects and want to differentiate them by keyPath. */ -- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjectDictionary:(NSDictionary*)dictionary; +- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjectDictionary:(NSDictionary *)dictionary; /** Invoked when the object loader has finished loading */ -- (void)objectLoaderDidFinishLoading:(RKObjectLoader*)objectLoader; +- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader; + +/** + 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. + */ +- (void)objectLoader:(RKObjectLoader *)objectLoader didSerializeSourceObject:(id)sourceObject toSerialization:(inout id *)serialization; /** 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; +- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader; /** Invoked just after parsing has completed, but before object mapping begins. This can be helpful 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. */ -- (void)objectLoader:(RKObjectLoader*)loader willMapData:(inout id *)mappableData; +- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData; @end @@ -105,20 +132,58 @@ * 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 { - RKObjectManager* _objectManager; - 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; + +/** + 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 + */ +@property (nonatomic, copy) RKObjectLoaderDidLoadObjectBlock onDidLoadObject; + +/** + 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 + */ +@property (nonatomic, copy) RKObjectLoaderDidLoadObjectsBlock onDidLoadObjects; + +/** + The block to invoke when the object loader has completed object mapping and the consumer + 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 + */ +@property (nonatomic, copy) RKObjectLoaderDidLoadObjectsDictionaryBlock onDidLoadObjectsDictionary; + /** * The object mapping to use when processing the response. If this is nil, * then RestKit will search the parsed response body for mappable keyPaths and @@ -129,26 +194,27 @@ * @default nil * @see RKObjectMappingProvider */ -// TODO: Rename to responseMapping -@property (nonatomic, retain) RKObjectMapping* objectMapping; +@property (nonatomic, retain) RKObjectMapping *objectMapping; /** - * The object manager that initialized this loader. The object manager is responsible - * for supplying the mapper and object store used after HTTP transport is completed + A mapping provider containing object mapping configurations for mapping remote + object representations into local domain objects. + + @see RKObjectMappingProvider */ -@property (nonatomic, readonly) RKObjectManager* objectManager; +@property (nonatomic, retain) RKObjectMappingProvider *mappingProvider; /** * 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 * object mapping has completed. Provides access to the final products of the * object mapper in a variety of formats. */ -@property (nonatomic, readonly) RKObjectMappingResult* result; +@property (nonatomic, readonly) RKObjectMappingResult *result; /////////////////////////////////////////////////////////////////////////////////////////// // Serialization @@ -159,8 +225,7 @@ * * @see RKObjectMappingProvider */ -// TODO: Rename to requestMapping? -@property (nonatomic, retain) RKObjectMapping* serializationMapping; +@property (nonatomic, retain) RKObjectMapping *serializationMapping; /** * The MIME Type to serialize the targetObject into according to the mapping @@ -169,36 +234,47 @@ * * @see RKMIMEType */ -@property (nonatomic, retain) NSString* serializationMIMEType; +@property (nonatomic, retain) NSString *serializationMIMEType; /** 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; +@property (nonatomic, retain) NSObject *sourceObject; /** * The target object to map results back onto. If nil, a new object instance * for the appropriate mapping will be created. If not nil, the results will * be used to update the targetObject's attributes and relationships. */ -@property (nonatomic, retain) NSObject* targetObject; +@property (nonatomic, retain) NSObject *targetObject; + +/** + The Grand Central Dispatch queue to perform our parsing and object mapping + within. By default, object loaders will use the mappingQueue from the RKObjectManager + that created the loader. You can override this on a per-loader basis as necessary. + */ +@property (nonatomic, assign) dispatch_queue_t mappingQueue; /////////////////////////////////////////////////////////////////////////////////////////// /** - * Initialize and return an object loader for a resource path against an object manager. The resource path - * specifies the remote location to load data from, while the object manager is responsible for supplying - * mapping and persistence details. + 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 */ -+ (id)loaderWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate; ++ (id)loaderWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider; /** - * Initialize a new object loader with an object manager, a request, and a delegate + 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 */ -- (id)initWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate; +- (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider; /** * Handle an error in the response preventing it from being mapped, called from -isResponseMappable @@ -206,3 +282,9 @@ - (void)handleResponseError; @end + +@class RKObjectManager; +@interface RKObjectLoader (Deprecations) ++ (id)loaderWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (id)initWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate DEPRECATED_ATTRIBUTE; +@end diff --git a/Code/ObjectMapping/RKObjectLoader.m b/Code/ObjectMapping/RKObjectLoader.m index e4882fea32..eed310762f 100644 --- a/Code/ObjectMapping/RKObjectLoader.m +++ b/Code/ObjectMapping/RKObjectLoader.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 8/8/09. -// Copyright 2009 Two Toasters -// +// 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,12 +22,10 @@ #import "RKObjectMapper.h" #import "RKObjectManager.h" #import "RKObjectMapperError.h" -#import "Errors.h" -#import "RKNotifications.h" -#import "RKParser.h" #import "RKObjectLoader_Internals.h" #import "RKParserRegistry.h" #import "RKRequest_Internals.h" +#import "RKObjectMappingProvider+Contexts.h" #import "RKObjectSerializer.h" // Set Logging Component @@ -36,113 +34,145 @@ @interface RKRequest (Private) - (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 objectManager = _objectManager, 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; - -+ (id)loaderWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate { - return [[[self alloc] initWithResourcePath:resourcePath objectManager:objectManager delegate:delegate] autorelease]; +@synthesize mappingQueue = _mappingQueue; +@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]; } -- (id)initWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate { - if ((self = [super initWithURL:[objectManager.client URLForResourcePath:resourcePath] delegate:delegate])) { - _objectManager = objectManager; - [self.objectManager.client setupRequest:self]; - } +- (id)initWithURL:(RKURL *)URL mappingProvider:(RKObjectMappingProvider *)mappingProvider { + self = [super initWithURL:URL]; + if (self) { + _mappingProvider = [mappingProvider retain]; + _mappingQueue = [RKObjectManager defaultMappingQueue]; + } - return self; + return self; } - (void)dealloc { - // Weak reference - _objectManager = nil; - + [_mappingProvider release]; + _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]; - - [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); + } +} + #pragma mark - Response Processing // 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 error:(NSError*)error { - _isLoading = NO; +- (void)finalizeLoad:(BOOL)successful { + self.loading = NO; + self.loaded = successful; - if (successful) { - _isLoaded = YES; - if ([self.delegate respondsToSelector:@selector(objectLoaderDidFinishLoading:)]) { - [(NSObject*)self.delegate performSelectorOnMainThread:@selector(objectLoaderDidFinishLoading:) - withObject:self waitUntilDone:YES]; - } - - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:_response - forKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification - object:self - userInfo:userInfo]; - } else { - NSDictionary* userInfo = [NSDictionary dictionaryWithObject:(error ? error : (NSError*)[NSNull null]) - forKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFailWithErrorNotification - object:self - userInfo:userInfo]; - } + if ([self.delegate respondsToSelector:@selector(objectLoaderDidFinishLoading:)]) { + [(NSObject*)self.delegate performSelectorOnMainThread:@selector(objectLoaderDidFinishLoading:) + withObject:self waitUntilDone:YES]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFinishLoadingNotification object:self]; } // 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]]; } - - [self finalizeLoad:YES error:nil]; + + if (self.onDidLoadObject) { + self.onDidLoadObject([result asObject]); + } + + [self finalizeLoad:YES]; } #pragma mark - Subclass Hooks /** Overloaded by RKManagedObjectLoader to serialize/deserialize managed objects - at thread boundaries. - + at thread boundaries. @protected */ - (void)processMappingResult:(RKObjectMappingResult*)result { @@ -152,92 +182,108 @@ - (void)processMappingResult:(RKObjectMappingResult*)result { #pragma mark - Response Object Mapping -- (RKObjectMappingResult*)mapResponseWithMappingProvider:(RKObjectMappingProvider*)mappingProvider toObject:(id)targetObject error:(NSError**)error { +- (RKObjectMappingResult*)mapResponseWithMappingProvider:(RKObjectMappingProvider*)mappingProvider toObject:(id)targetObject inContext:(RKObjectMappingProviderContext)context error:(NSError**)error { id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:self.response.MIMEType]; NSAssert1(parser, @"Cannot perform object load without a parser for MIME Type '%@'", self.response.MIMEType); - + // Check that there is actually content in the response body for mapping. It is possible to get back a 200 response // 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); 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]]; } - + id parsedData = [parser objectFromString:bodyAsString error:error]; if (parsedData == nil && error) { return nil; } - + // Allow the delegate to manipulate the data if ([self.delegate respondsToSelector:@selector(objectLoader:willMapData:)]) { parsedData = [[parsedData mutableCopy] autorelease]; [(NSObject*)self.delegate objectLoader:self willMapData:&parsedData]; } - + RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:parsedData mappingProvider:mappingProvider]; mapper.targetObject = targetObject; mapper.delegate = self; + mapper.context = context; RKObjectMappingResult* result = [mapper performMapping]; - + // Log any mapping errors if (mapper.errorCount > 0) { RKLogError(@"Encountered errors during mapping: %@", [[mapper.errors valueForKey:@"localizedDescription"] componentsJoinedByString:@", "]); } - + // The object mapper will return a nil result if mapping failed if (nil == result) { // TODO: Construct a composite error that wraps up all the other errors. Should probably make it performMapping:&error when we have this? if (error) *error = [mapper.errors lastObject]; return nil; } - + return result; } +- (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; - if (self.objectMapping) { - NSString* rootKeyPath = self.objectMapping.rootKeyPath ? self.objectMapping.rootKeyPath : @""; - RKLogDebug(@"Found directly configured object mapping, creating temporary mapping provider for keyPath %@", rootKeyPath); - mappingProvider = [[RKObjectMappingProvider new] autorelease]; - [mappingProvider setMapping:self.objectMapping forKeyPath:rootKeyPath]; + 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 { RKLogDebug(@"No object mapping provider, using mapping provider from parent object manager to perform KVC mapping"); - mappingProvider = self.objectManager.mappingProvider; + mappingProvider = self.mappingProvider; } - - return [self mapResponseWithMappingProvider:mappingProvider toObject:self.targetObject error:error]; + + return [self mapResponseWithMappingProvider:mappingProvider toObject:self.targetObject inContext:RKObjectMappingProviderContextObjectsByKeyPath error:error]; } - -- (void)performMappingOnBackgroundThread { - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - - NSError* error = nil; - _result = [[self performMapping:&error] retain]; - NSAssert(_result || error, @"Expected performMapping to return a mapping result or an error."); - if (self.result) { - [self processMappingResult:self.result]; - } else if (error) { - [self didFailLoadWithError:error]; - } +- (void)performMappingInDispatchQueue { + NSAssert(self.mappingQueue, @"mappingQueue cannot be nil"); + dispatch_async(self.mappingQueue, ^{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + RKLogDebug(@"Beginning object mapping activities within GCD queue labeled: %s", dispatch_queue_get_label(self.mappingQueue)); + NSError *error = nil; + _result = [[self performMapping:&error] retain]; + NSAssert(_result || error, @"Expected performMapping to return a mapping result or an error."); + if (self.result) { + [self processMappingResult:self.result]; + } else if (error) { + [self performSelectorOnMainThread:@selector(didFailLoadWithError:) withObject:error waitUntilDone:NO]; + } - [pool drain]; + [pool drain]; + }); } - (BOOL)canParseMIMEType:(NSString*)MIMEType { if ([[RKParserRegistry sharedRegistry] parserForMIMEType:self.response.MIMEType]) { return YES; } - + RKLogWarning(@"Unable to find parser for MIME Type '%@'", MIMEType); return NO; } @@ -246,54 +292,61 @@ - (BOOL)isResponseMappable { if ([self.response isServiceUnavailable]) { [[NSNotificationCenter defaultCenter] postNotificationName:RKServiceDidBecomeUnavailableNotification object:self]; } - - if ([self.response isFailure]) { - [(NSObject*)_delegate objectLoader:self didFailWithError:self.response.failureError]; - - [self finalizeLoad:NO error:self.response.failureError]; - - return NO; + + if ([self.response isFailure]) { + [self informDelegateOfError:self.response.failureError]; + + [self didFailLoadWithError:self.response.failureError]; + return NO; } else if ([self.response isNoContent]) { - // The No Content (204) response will never have a message body or a MIME Type. Invoke the delegate with self - [self informDelegateOfObjectLoadWithResultDictionary:[NSDictionary dictionaryWithObject:self forKey:@""]]; + // The No Content (204) response will never have a message body or a MIME Type. + id resultDictionary = nil; + if (self.targetObject) { + resultDictionary = [NSDictionary dictionaryWithObject:self.targetObject forKey:@""]; + } else if (self.sourceObject) { + resultDictionary = [NSDictionary dictionaryWithObject:self.sourceObject forKey:@""]; + } else { + resultDictionary = [NSDictionary dictionary]; + } + [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: %@)", (long) self.response.statusCode, self.response.MIMEType); - NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKObjectLoaderUnexpectedResponseError userInfo:nil]; + 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]; if ([_delegate respondsToSelector:@selector(objectLoaderDidLoadUnexpectedResponse:)]) { [(NSObject*)_delegate objectLoaderDidLoadUnexpectedResponse:self]; - } else { - [(NSObject*)_delegate objectLoader:self didFailWithError:error]; + } else { + [self informDelegateOfError:error]; } - + // NOTE: We skip didFailLoadWithError: here so that we don't send the delegate // conflicting messages around unexpected response and failure with error - [self finalizeLoad:NO error:error]; - + [self finalizeLoad:NO]; + return NO; } 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 { // Since we are mapping what we know to be an error response, we don't want to map the result back onto our // target object - NSError* error = nil; - RKObjectMappingResult* result = [self mapResponseWithMappingProvider:self.objectManager.mappingProvider toObject:nil error:&error]; + NSError *error = nil; + RKObjectMappingResult *result = [self mapResponseWithMappingProvider:self.mappingProvider toObject:nil inContext:RKObjectMappingProviderContextErrors error:&error]; if (result) { error = [result asError]; } else { RKLogError(@"Encountered an error while attempting to map server side errors from payload: %@", [error localizedDescription]); } - - [(NSObject*)_delegate objectLoader:self didFailWithError:error]; - [self finalizeLoad:NO error:error]; + + [self informDelegateOfError:error]; + [self finalizeLoad:NO]; } #pragma mark - RKRequest & RKRequestDelegate methods @@ -305,68 +358,97 @@ - (BOOL)prepareURLRequest { RKLogDebug(@"POST or PUT request for source object %@, serializing to MIME Type %@ for transport...", self.sourceObject, self.serializationMIMEType); RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:self.sourceObject mapping:self.serializationMapping]; NSError* error = nil; - id params = [serializer serializationForMIMEType:self.serializationMIMEType error:&error]; - + id params = [serializer serializationForMIMEType:self.serializationMIMEType error:&error]; + if (error) { RKLogError(@"Serializing failed for source object %@ to MIME Type %@: %@", self.sourceObject, self.serializationMIMEType, [error localizedDescription]); [self didFailLoadWithError:error]; return NO; } - + + if ([self.delegate respondsToSelector:@selector(objectLoader:didSerializeSourceObject:toSerialization:)]) { + [self.delegate objectLoader:self didSerializeSourceObject:self.sourceObject toSerialization:¶ms]; + } + self.params = params; } - + // TODO: This is an informal protocol ATM. Maybe its not obvious enough? if (self.sourceObject) { if ([self.sourceObject respondsToSelector:@selector(willSendWithObjectLoader:)]) { [self.sourceObject performSelector:@selector(willSendWithObjectLoader:) withObject:self]; } } - + return [super prepareURLRequest]; } -- (void)didFailLoadWithError:(NSError*)error { +- (void)didFailLoadWithError:(NSError *)error { + NSParameterAssert(error); NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - - if (_cachePolicy & RKRequestCachePolicyLoadOnError && - [self.cache hasResponseForRequest:self]) { - [self didFinishLoad:[self.cache responseForRequest:self]]; - } else { + if (_cachePolicy & RKRequestCachePolicyLoadOnError && + [self.cache hasResponseForRequest:self]) { + + [self didFinishLoad:[self.cache responseForRequest:self]]; + } else { if ([_delegate respondsToSelector:@selector(request:didFailLoadWithError:)]) { [_delegate request:self didFailLoadWithError:error]; } + + if (self.onDidFailLoadWithError) { + self.onDidFailLoadWithError(error); + } + + // If we failed due to a transport error or before we have a response, the request itself failed + if (!self.response || [self.response isFailure]) { + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:error forKey:RKRequestDidFailWithErrorNotificationUserInfoErrorKey]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidFailWithErrorNotification + object:self + userInfo:userInfo]; + } - [(NSObject*)_delegate objectLoader:self didFailWithError:error]; - - [self finalizeLoad:NO error:error]; + if (! self.isCancelled) { + [self informDelegateOfError:error]; + } + + [self finalizeLoad:NO]; } - + [pool release]; } // 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]; + 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 isResponseMappable]) { + + if (self.onDidLoadResponse) { + self.onDidLoadResponse(self.response); + } + + // Post the notification + NSDictionary* userInfo = [NSDictionary dictionaryWithObject:self.response + forKey:RKRequestDidLoadResponseNotificationUserInfoResponseKey]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKRequestDidLoadResponseNotification + object:self + userInfo:userInfo]; + + if ([self isResponseMappable]) { // Determine if we are synchronous here or not. if (_sentSynchronously) { NSError* error = nil; @@ -377,9 +459,48 @@ - (void)didFinishLoad:(RKResponse*)response { [self performSelectorInBackground:@selector(didFailLoadWithError:) withObject:error]; } } else { - [self performSelectorInBackground:@selector(performMappingOnBackgroundThread) withObject:nil]; + [self performMappingInDispatchQueue]; } - } + } +} + +- (void)setMappingQueue:(dispatch_queue_t)newMappingQueue { + if (_mappingQueue) { + dispatch_release(_mappingQueue); + _mappingQueue = nil; + } + + if (newMappingQueue) { + dispatch_retain(newMappingQueue); + _mappingQueue = newMappingQueue; + } +} + +// Proxy the delegate property back to our superclass implementation. The object loader should +// really not be a subclass of RKRequest. +- (void)setDelegate:(id)delegate { + [super setDelegate:delegate]; +} + +- (id)delegate { + return (id) [super delegate]; +} + +@end + +@implementation RKObjectLoader (Deprecations) + ++ (id)loaderWithResourcePath:(NSString*)resourcePath objectManager:(RKObjectManager*)objectManager delegate:(id)delegate { + 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])) { + [objectManager.client configureRequest:self]; + _delegate = theDelegate; + } + + return self; } @end diff --git a/Code/ObjectMapping/RKObjectLoader_Internals.h b/Code/ObjectMapping/RKObjectLoader_Internals.h index 23ad49c5fd..7f745bed8b 100644 --- a/Code/ObjectMapping/RKObjectLoader_Internals.h +++ b/Code/ObjectMapping/RKObjectLoader_Internals.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/13/11. -// Copyright 2011 Two Toasters -// +// 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,9 @@ @property (nonatomic, readonly) RKClient* client; - (void)handleTargetObject; -- (void)informDelegateOfObjectLoadWithInfoDictionary:(NSDictionary*)dictionary; +- (void)informDelegateOfObjectLoadWithResultDictionary:(NSDictionary*)dictionary; - (void)performMappingOnBackgroundThread; +- (BOOL)isResponseMappable; +- (void)finalizeLoad:(BOOL)successful error:(NSError*)error; @end diff --git a/Code/ObjectMapping/RKObjectManager.h b/Code/ObjectMapping/RKObjectManager.h index 2d23ebb148..fc6f3adc23 100644 --- a/Code/ObjectMapping/RKObjectManager.h +++ b/Code/ObjectMapping/RKObjectManager.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/14/09. -// Copyright 2009 Two Toasters -// +// 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,18 +22,28 @@ #import "RKObjectLoader.h" #import "RKObjectRouter.h" #import "RKObjectMappingProvider.h" +#import "RKConfigurationDelegate.h" +#import "RKObjectPaginator.h" @protocol RKParser; -// Notifications -extern NSString* const RKDidEnterOfflineModeNotification; -extern NSString* const RKDidEnterOnlineModeNotification; +/** Notifications */ + +/** + Posted when the object managed has transitioned to the offline state + */ +extern NSString* const RKObjectManagerDidBecomeOfflineNotification; + +/** + Posted when the object managed has transitioned to the online state + */ +extern NSString* const RKObjectManagerDidBecomeOnlineNotification; typedef enum { - RKObjectManagerOnlineStateUndetermined, - RKObjectManagerOnlineStateDisconnected, - RKObjectManagerOnlineStateConnected -} RKObjectManagerOnlineState; + RKObjectManagerNetworkStatusUnknown, + RKObjectManagerNetworkStatusOffline, + RKObjectManagerNetworkStatusOnline +} RKObjectManagerNetworkStatus; @class RKManagedObjectStore; @@ -43,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]; @@ -88,44 +98,59 @@ 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 - */ -@interface RKObjectManager : NSObject { - RKClient* _client; - RKObjectRouter* _router; - RKManagedObjectStore* _objectStore; - RKObjectManagerOnlineState _onlineState; - RKObjectMappingProvider* _mappingProvider; - NSString* _serializationMIMEType; - BOOL _inferMappingsFromObjectTypes; -} + instance configured + */ +@interface RKObjectManager : NSObject /// @name Configuring the Shared Manager Instance /** Return the shared instance of the object manager */ -+ (RKObjectManager*)sharedManager; ++ (RKObjectManager *)sharedManager; /** Set the shared instance of the object manager */ -+ (void)setSharedManager:(RKObjectManager*)manager; ++ (void)setSharedManager:(RKObjectManager *)manager; + +/** @name Object Mapping Dispatch Queue */ + +/** + Returns the global default Grand Central Dispatch queue used for object mapping + operations executed by RKObjectLoaders. + + All object loaders perform their loading within a Grand Central Dispatch + queue. This provides control over the number of loaders that are performing + expensive operations such as JSON parsing, object mapping, and accessing Core + Data concurrently. The defaultMappingQueue is configured as the mappingQueue + for all RKObjectManager's created by RestKit, but can be overridden on a per + manager and per object loader basis. + + By default, the defaultMappingQueue is configured as serial GCD queue. + */ ++ (dispatch_queue_t)defaultMappingQueue; + +/** + Sets a new global default Grand Central Dispatch queue for use in object mapping + operations executed by RKObjectLoaders. + */ ++ (void)setDefaultMappingQueue:(dispatch_queue_t)defaultMappingQueue; /// @name Initializing an Object Manager @@ -133,79 +158,161 @@ typedef enum { Create and initialize a new object manager. If this is the first instance created it will be set as the shared instance */ -+ (RKObjectManager*)objectManagerWithBaseURL:(NSString*)baseURL; ++ (id)managerWithBaseURLString:(NSString *)baseURLString; ++ (id)managerWithBaseURL:(NSURL *)baseURL; /** - Initialize a new model manager instance + 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 */ -- (id)initWithBaseURL:(NSString*)baseURL; +- (id)initWithBaseURL:(RKURL *)baseURL; /// @name Network Integration /** The underlying HTTP client for this manager */ -@property (nonatomic, retain) RKClient* client; +@property (nonatomic, retain) RKClient *client; + +/** + 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. + */ +@property (nonatomic, readonly) RKURL *baseURL; /** The request cache used to store and load responses for requests sent through this object manager's underlying client object */ -@property (nonatomic, readonly) RKRequestQueue *requestQueue; +@property (nonatomic, readonly) RKRequestCache *requestCache; /** The request queue used to dispatch asynchronous requests sent through this object manager's underlying client object */ -@property (nonatomic, readonly) RKRequestCache *requestCache; +@property (nonatomic, readonly) RKRequestQueue *requestQueue; /** - True when we are in online mode + Returns the current network status for this object manager as determined + by connectivity to the remote backend system */ -- (BOOL)isOnline; +@property (nonatomic, readonly) RKObjectManagerNetworkStatus networkStatus; + +/** + Returns YES when we are in online mode + */ +@property (nonatomic, readonly) BOOL isOnline; + +/** + Returns YES when we are in offline mode + */ +@property (nonatomic, readonly) BOOL isOffline; /// @name Configuring Object Mapping /** The Mapping Provider responsible for returning mappings for various keyPaths. */ -@property (nonatomic, retain) RKObjectMappingProvider* mappingProvider; +@property (nonatomic, retain) RKObjectMappingProvider *mappingProvider; /** Router object responsible for generating resource paths for HTTP requests */ -@property (nonatomic, retain) RKObjectRouter* router; +@property (nonatomic, retain) RKObjectRouter *router; /** A Core Data backed object store for persisting objects that have been fetched from the Web */ -@property (nonatomic, retain) RKManagedObjectStore* objectStore; +@property (nonatomic, retain) RKManagedObjectStore *objectStore; + +/** + The Grand Dispatch Queue to use when performing expensive object mapping operations + within RKObjectLoader instances created through this object manager + */ +@property (nonatomic, assign) dispatch_queue_t mappingQueue; /** The Default MIME Type to be used in object serialization. */ -@property (nonatomic, retain) NSString* serializationMIMEType; +@property (nonatomic, retain) NSString *serializationMIMEType; /** The value for the HTTP Accept header to specify the preferred format for retrieved data */ -@property (nonatomic, assign) NSString* acceptMIMEType; +@property (nonatomic, assign) NSString *acceptMIMEType; + +//////////////////////////////////////////////////////// +/// @name Building Object Loaders + +/** + 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 + @see RKClient + */ +- (id)loaderWithResourcePath:(NSString *)resourcePath; + +/** + 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 + @see RKClient + */ +- (id)loaderWithURL:(NSURL *)URL; /** - When YES, RestKit will auto-select the appropriate object mapping for a particular object - passed through getObject:, postObject:, putObject:, and deleteObject:. - - This is useful when you are working with mappable data that is not identifiable via KVC - and you are sending/receiving objects of the same type. When YES, RestKit will search the - mappingProvider for an object mapping targeting the same type of object that you passed into - getObject:, postObject:, :putObject, or deleteObject: and configure the RKObjectLoader to map - the payload using that mapping. This is merely a convenience for users who are working entirely - with non-KVC mappable data and saves the added step of searching the mapping provider manually. - - Default: NO + 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 + 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 + @see RKObjectRouter */ -@property (nonatomic, assign) BOOL inferMappingsFromObjectTypes; +- (id)loaderForObject:(id)object method:(RKRequestMethod)method; + +/** + 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. + + @return The newly created paginator instance. + @see RKObjectMappingProvider + @see RKObjectPaginator + */ +- (RKObjectPaginator *)paginatorWithResourcePathPattern:(NSString *)resourcePathPattern; //////////////////////////////////////////////////////// /// @name Registered Object Loaders @@ -215,41 +322,36 @@ 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 set on the mapping provider. */ -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath delegate:(id)delegate; - -/** - Load mappable objects at the specified resourcePath using the specified object mapping. - */ -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath objectMapping:(RKObjectMapping*)objectMapping delegate:(id)delegate; +- (void)loadObjectsAtResourcePath:(NSString *)resourcePath delegate:(id)delegate; //////////////////////////////////////////////////////// /// @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. */ -- (RKObjectLoader*)getObject:(id)object delegate:(id)delegate; +- (void)getObject:(id)object delegate:(id)delegate; /** Create a remote mappable model by POSTing the attributes to the remote resource and loading the resulting objects from the payload */ -- (RKObjectLoader*)postObject:(id)object delegate:(id)delegate; +- (void)postObject:(id)object delegate:(id)delegate; /** Update a remote mappable model by PUTing the attributes to the remote resource and loading the resulting objects from the payload */ -- (RKObjectLoader*)putObject:(id)object delegate:(id)delegate; +- (void)putObject:(id)object delegate:(id)delegate; /** Delete the remote instance of a mappable model by performing an HTTP DELETE on the remote resource */ -- (RKObjectLoader*)deleteObject:(id)object delegate:(id)delegate; +- (void)deleteObject:(id)object delegate:(id)delegate; //////////////////////////////////////////////////////// /// @name Block Configured Object Loaders @@ -260,121 +362,93 @@ 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)loadObjectWithBlockExample { - [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/monkeys.json" delegate:self block:^(RKObjectLoader* loader) { + + - (void)loadObjectUsingBlockExample { + [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/monkeys.json" usingBlock:^(RKObjectLoader* loader) { loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[Monkey class]]; }]; } */ -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath delegate:(id)delegate block:(void(^)(RKObjectLoader*))block; +- (void)loadObjectsAtResourcePath:(NSString *)resourcePath usingBlock:(RKObjectLoaderBlock)block; -/** +/* 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; - [[RKObjectManager sharedManager] sendObject:self delegate:self block:^(RKObjectLoader* loader) { + [[RKObjectManager sharedManager] sendObject:self toResourcePath:@"/some/path" usingBlock:^(RKObjectLoader* loader) { + loader.delegate = self; loader.method = RKRequestMethodPOST; loader.serializationMIMEType = RKMIMETypeJSON; // We want to send this request as JSON loader.targetObject = nil; // Map the results back onto a new object instead of self // Set up a custom serialization mapping to handle this request - loader.serializationMapping = [RKObjectMapping serializationMappingWithBlock:^(RKObjectMapping* mapping) { + loader.serializationMapping = [RKObjectMapping serializationMappingUsingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"password", nil]; }]; }]; } } */ -- (RKObjectLoader*)sendObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block; +- (void)sendObject:(id)object toResourcePath:(NSString *)resourcePath usingBlock:(RKObjectLoaderBlock)block; /** GET a remote object instance and yield the object loader to the block before sending - + @see sendObject:method:delegate:block */ -- (RKObjectLoader*)getObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))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 - - (RKObjectLoader*)postObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block; */ -- (RKObjectLoader*)postObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))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 */ -- (RKObjectLoader*)putObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))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 */ -- (RKObjectLoader*)deleteObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block; +- (void)deleteObject:(id)object usingBlock:(RKObjectLoaderBlock)block; #endif ////// -/** - Fetch the data for a mappable object by performing an HTTP GET. The data returned in the response will be mapped according - to the object mapping provided. - */ -- (RKObjectLoader*)getObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate; - -/** - Send the data for a mappable object by performing an HTTP POST. The data returned in the response will be mapped according - to the object mapping provided. - */ -- (RKObjectLoader*)postObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate; -/** - Send the data for a mappable object by performing an HTTP PUT. The data returned in the response will be mapped according - to the object mapping provided. - */ -- (RKObjectLoader*)putObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate; +// Deprecations -/** - Delete a remote object representation by performing an HTTP DELETE. The data returned in the response will be mapped according - to the object mapping provided. - */ -- (RKObjectLoader*)deleteObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate; ++ (RKObjectManager *)objectManagerWithBaseURLString:(NSString *)baseURLString; ++ (RKObjectManager *)objectManagerWithBaseURL:(NSURL *)baseURL; +- (void)loadObjectsAtResourcePath:(NSString*)resourcePath objectMapping:(RKObjectMapping*)objectMapping delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (RKObjectLoader *)objectLoaderWithResourcePath:(NSString*)resourcePath delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (RKObjectLoader *)objectLoaderForObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate DEPRECATED_ATTRIBUTE; -/** - These methods are provided for situations where the remote system you are working with has slightly different conventions - than the default methods provide. They return fully initialized object loaders that are ready for dispatch, but - have not yet been sent. This can be used to add one-off params to the request body or otherwise manipulate the request - before it is sent off to be loaded & object mapped. This can also be used to perform a synchronous object load. - */ - -/** - Return an object loader ready to be sent. The method defaults to GET and the URL is relative to the - baseURL configured on the client. The loader is configured for an implicit objectClass load. This is - the best place to begin work if you need to create a slightly different collection loader than what is - provided by the loadObjects family of methods. - */ -- (RKObjectLoader*)objectLoaderWithResourcePath:(NSString*)resourcePath delegate:(id)delegate; +/* + NOTE: -/** - Returns an object loader configured for transmitting an object instance across the wire. A request will be constructed - for you with the resource path configured for you by the Router. This is the best place to - begin work if you need a slightly different interaction with the server than what is provided for you by get/post/put/delete - object family of methods. Note that this should be used for one-off changes. If you need to substantially modify all your - object loads, you are better off subclassing or implementing your own RKRouter for dryness. - - // TODO: Cleanup this comment + The mapResponseWith: family of methods have been deprecated by the support for object mapping selection + using resourcePath's */ - (RKObjectLoader*)objectLoaderForObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate; +- (RKObjectLoader *)objectLoaderForObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate; +- (void)getObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (void)postObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (void)putObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate DEPRECATED_ATTRIBUTE; +- (void)deleteObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate DEPRECATED_ATTRIBUTE; @end diff --git a/Code/ObjectMapping/RKObjectManager.m b/Code/ObjectMapping/RKObjectManager.m index c2e3e732f3..846ffb3318 100644 --- a/Code/ObjectMapping/RKObjectManager.m +++ b/Code/ObjectMapping/RKObjectManager.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/14/09. -// Copyright 2009 Two Toasters -// +// 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,16 +25,21 @@ #import "Support.h" #import "RKErrorMessage.h" -NSString* const RKDidEnterOfflineModeNotification = @"RKDidEnterOfflineModeNotification"; -NSString* const RKDidEnterOnlineModeNotification = @"RKDidEnterOnlineModeNotification"; +NSString* const RKObjectManagerDidBecomeOfflineNotification = @"RKDidEnterOfflineModeNotification"; +NSString* const RKObjectManagerDidBecomeOnlineNotification = @"RKDidEnterOnlineModeNotification"; ////////////////////////////////// -// Shared Instance +// Shared Instances -static RKObjectManager* sharedManager = nil; +static RKObjectManager *sharedManager = nil; +static dispatch_queue_t defaultMappingQueue = nil; /////////////////////////////////// +@interface RKObjectManager () +@property (nonatomic, assign, readwrite) RKObjectManagerNetworkStatus networkStatus; +@end + @implementation RKObjectManager @synthesize client = _client; @@ -42,292 +47,337 @@ @implementation RKObjectManager @synthesize router = _router; @synthesize mappingProvider = _mappingProvider; @synthesize serializationMIMEType = _serializationMIMEType; -@synthesize inferMappingsFromObjectTypes = _inferMappingsFromObjectTypes; +@synthesize networkStatus = _networkStatus; +@synthesize mappingQueue = _mappingQueue; + ++ (dispatch_queue_t)defaultMappingQueue { + if (! defaultMappingQueue) { + defaultMappingQueue = dispatch_queue_create("org.restkit.ObjectMapping", DISPATCH_QUEUE_SERIAL); + } + + return defaultMappingQueue; +} -- (id)initWithBaseURL:(NSString*)baseURL { ++ (void)setDefaultMappingQueue:(dispatch_queue_t)newDefaultMappingQueue { + if (defaultMappingQueue) { + dispatch_release(defaultMappingQueue); + defaultMappingQueue = nil; + } + + if (newDefaultMappingQueue) { + dispatch_retain(newDefaultMappingQueue); + defaultMappingQueue = newDefaultMappingQueue; + } +} + +- (id)init { self = [super init]; - if (self) { + if (self) { _mappingProvider = [RKObjectMappingProvider new]; - _router = [RKObjectRouter new]; - _client = [[RKClient clientWithBaseURL:baseURL] retain]; - _onlineState = RKObjectManagerOnlineStateUndetermined; - _inferMappingsFromObjectTypes = NO; - - self.acceptMIMEType = RKMIMETypeJSON; + _router = [RKObjectRouter new]; + _networkStatus = RKObjectManagerNetworkStatusUnknown; + self.serializationMIMEType = RKMIMETypeFormURLEncoded; - + self.mappingQueue = [RKObjectManager defaultMappingQueue]; + // Setup default error message mappings - RKObjectMapping* errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; + RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; + errorMapping.rootKeyPath = @"errors"; [errorMapping mapKeyPath:@"" toAttribute:@"errorMessage"]; - [_mappingProvider setMapping:errorMapping forKeyPath:@"error"]; - [_mappingProvider setMapping:errorMapping forKeyPath:@"errors"]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reachabilityChanged:) - name:RKReachabilityDidChangeNotification - object:_client.reachabilityObserver]; - } - - return self; + _mappingProvider.errorMapping = errorMapping; + + [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; +} + +- (id)initWithBaseURL:(RKURL *)baseURL { + self = [self init]; + if (self) { + self.client = [RKClient clientWithBaseURL:baseURL]; + self.acceptMIMEType = RKMIMETypeJSON; + } + + return self; } -+ (RKObjectManager*)sharedManager { - return sharedManager; ++ (RKObjectManager *)sharedManager { + return sharedManager; } -+ (void)setSharedManager:(RKObjectManager*)manager { - [manager retain]; - [sharedManager release]; - sharedManager = manager; ++ (void)setSharedManager:(RKObjectManager *)manager { + [manager retain]; + [sharedManager release]; + sharedManager = manager; } -+ (RKObjectManager*)objectManagerWithBaseURL:(NSString*)baseURL { - RKObjectManager* manager = [[[self alloc] initWithBaseURL:baseURL] autorelease]; - if (nil == sharedManager) { - [RKObjectManager setSharedManager:manager]; - } - return manager; ++ (RKObjectManager *)managerWithBaseURLString:(NSString *)baseURLString { + return [self managerWithBaseURL:[RKURL URLWithString:baseURLString]]; +} + ++ (RKObjectManager *)managerWithBaseURL:(NSURL *)baseURL { + RKObjectManager *manager = [[[self alloc] initWithBaseURL:baseURL] autorelease]; + return manager; } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [_router release]; - _router = nil; - [_client release]; - _client = nil; - [_objectStore release]; - _objectStore = nil; + [self removeObserver:self forKeyPath:@"client.reachabilityObserver"]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_router release]; + _router = nil; + self.client = nil; + [_objectStore release]; + _objectStore = nil; [_serializationMIMEType release]; _serializationMIMEType = nil; [_mappingProvider release]; _mappingProvider = nil; - - [super dealloc]; + + [super dealloc]; } - (BOOL)isOnline { - return (_onlineState == RKObjectManagerOnlineStateConnected); + return (_networkStatus == RKObjectManagerNetworkStatusOnline); } - (BOOL)isOffline { - return ![self isOnline]; + 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]; +- (void)reachabilityChanged:(NSNotification *)notification { + BOOL isHostReachable = [self.client.reachabilityObserver isNetworkReachable]; - _onlineState = isHostReachable ? RKObjectManagerOnlineStateConnected : RKObjectManagerOnlineStateDisconnected; + _networkStatus = isHostReachable ? RKObjectManagerNetworkStatusOnline : RKObjectManagerNetworkStatusOffline; - if (isHostReachable) { - [[NSNotificationCenter defaultCenter] postNotificationName:RKDidEnterOnlineModeNotification object:self]; - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:RKDidEnterOfflineModeNotification object:self]; - } + if (isHostReachable) { + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:self]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:self]; + } } -- (void)setAcceptMIMEType:(NSString*)MIMEType { +- (void)setAcceptMIMEType:(NSString *)MIMEType { [_client setValue:MIMEType forHTTPHeaderField:@"Accept"]; } -- (NSString*)acceptMIMEType { +- (NSString *)acceptMIMEType { return [self.client.HTTPHeaders valueForKey:@"Accept"]; } ///////////////////////////////////////////////////////////// #pragma mark - Object Collection Loaders -- (RKObjectLoader*)objectLoaderWithResourcePath:(NSString*)resourcePath delegate:(id)delegate { - RKObjectLoader* objectLoader = nil; +- (Class)objectLoaderClass { Class managedObjectLoaderClass = NSClassFromString(@"RKManagedObjectLoader"); if (self.objectStore && managedObjectLoaderClass) { - objectLoader = [managedObjectLoaderClass loaderWithResourcePath:resourcePath objectManager:self delegate:delegate]; - } else { - objectLoader = [RKObjectLoader loaderWithResourcePath:resourcePath objectManager:self delegate:delegate]; - } - - return objectLoader; -} - -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderWithResourcePath:resourcePath delegate:delegate]; - loader.method = RKRequestMethodGET; + return managedObjectLoaderClass; + } - [loader send]; + return [RKObjectLoader class]; +} - return loader; +- (id)loaderWithResourcePath:(NSString *)resourcePath { + RKURL *URL = [self.baseURL URLByAppendingResourcePath:resourcePath]; + return [self loaderWithURL:URL]; } -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath objectMapping:(RKObjectMapping*)objectMapping delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderWithResourcePath:resourcePath delegate:delegate]; - loader.method = RKRequestMethodGET; - loader.objectMapping = objectMapping; +- (id)loaderWithURL:(RKURL *)URL { + RKObjectLoader *loader = [[self objectLoaderClass] loaderWithURL:URL mappingProvider:self.mappingProvider]; + loader.configurationDelegate = self; + if ([loader isKindOfClass:[RKManagedObjectLoader class]]) { + [(RKManagedObjectLoader *)loader setObjectStore:self.objectStore]; + } + [self configureObjectLoader:loader]; - [loader send]; + return loader; +} - return loader; +- (NSURL *)baseURL { + return self.client.baseURL; } -///////////////////////////////////////////////////////////// -#pragma mark - Object Instance Loaders +- (RKObjectPaginator *)paginatorWithResourcePathPattern:(NSString *)resourcePathPattern { + RKURL *patternURL = [[self baseURL] URLByAppendingResourcePath:resourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL + mappingProvider:self.mappingProvider]; + paginator.configurationDelegate = self; + return paginator; +} -- (RKObjectLoader*)objectLoaderForObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate { - NSString* resourcePath = [self.router resourcePathForObject:object method:method]; - RKObjectLoader* loader = [self objectLoaderWithResourcePath:resourcePath delegate:delegate]; +- (id)loaderForObject:(id)object method:(RKRequestMethod)method { + NSString* resourcePath = (method == RKRequestMethodInvalid) ? nil : [self.router resourcePathForObject:object method:method]; + RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; loader.method = method; loader.sourceObject = object; - loader.targetObject = object; loader.serializationMIMEType = self.serializationMIMEType; loader.serializationMapping = [self.mappingProvider serializationMappingForClass:[object class]]; - - if (self.inferMappingsFromObjectTypes) { - RKObjectMapping* objectMapping = [self.mappingProvider objectMappingForClass:[object class]]; - RKLogDebug(@"Auto-selected object mapping %@ for object of type %@", objectMapping, NSStringFromClass([object class])); - loader.objectMapping = objectMapping; + + 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; } -- (RKObjectLoader*)getObject:(id)object delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodGET delegate:delegate]; - [loader send]; - return loader; +- (void)loadObjectsAtResourcePath:(NSString *)resourcePath delegate:(id)delegate { + RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; + loader.delegate = delegate; + loader.method = RKRequestMethodGET; + + [loader send]; } -- (RKObjectLoader*)postObject:(id)object delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodPOST delegate:delegate]; - [loader send]; - return loader; +///////////////////////////////////////////////////////////// +#pragma mark - Object Instance Loaders + +- (void)getObject:(id)object delegate:(id)delegate { + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodGET]; + loader.delegate = delegate; + [loader send]; } -- (RKObjectLoader*)putObject:(id)object delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodPUT delegate:delegate]; - [loader send]; - return loader; +- (void)postObject:(id)object delegate:(id)delegate { + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPOST]; + loader.delegate = delegate; + [loader send]; } -- (RKObjectLoader*)deleteObject:(id)object delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodDELETE delegate:delegate]; - [loader send]; - return loader; +- (void)putObject:(id)object delegate:(id)delegate { + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodPUT]; + loader.delegate = delegate; + [loader send]; +} + +- (void)deleteObject:(id)object delegate:(id)delegate { + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodDELETE]; + loader.delegate = delegate; + [loader send]; } #if NS_BLOCKS_AVAILABLE #pragma mark - Block Configured Object Loaders -- (RKObjectLoader*)loadObjectsAtResourcePath:(NSString*)resourcePath delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - RKObjectLoader* loader = [self objectLoaderWithResourcePath:resourcePath delegate:delegate]; - loader.method = RKRequestMethodGET; - +- (void)loadObjectsAtResourcePath:(NSString*)resourcePath usingBlock:(void(^)(RKObjectLoader *))block { + RKObjectLoader* loader = [self loaderWithResourcePath:resourcePath]; + loader.method = RKRequestMethodGET; + // Yield to the block for setup block(loader); - - [loader send]; - - return loader; + + [loader send]; } -- (RKObjectLoader*)sendObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - RKObjectLoader* loader = [self objectLoaderWithResourcePath:nil delegate:delegate]; - loader.sourceObject = object; - loader.targetObject = object; - loader.serializationMIMEType = self.serializationMIMEType; - loader.serializationMapping = [self.mappingProvider serializationMappingForClass:[object class]]; - +- (void)sendObject:(id)object toResourcePath:(NSString *)resourcePath usingBlock:(void(^)(RKObjectLoader *))block { + RKObjectLoader *loader = [self loaderForObject:object method:RKRequestMethodInvalid]; + loader.URL = [self.baseURL URLByAppendingResourcePath:resourcePath]; // Yield to the block for setup block(loader); - - if (loader.resourcePath == nil) { - loader.resourcePath = [self.router resourcePathForObject:object method:loader.method]; - } - - if (loader.objectMapping == nil) { - if (self.inferMappingsFromObjectTypes) { - RKObjectMapping* objectMapping = [self.mappingProvider objectMappingForClass:[object class]]; - RKLogDebug(@"Auto-selected object mapping %@ for object of type %@", objectMapping, NSStringFromClass([object class])); - loader.objectMapping = objectMapping; - } - } - + [loader send]; - return loader; } - -- (RKObjectLoader*)sendObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - return [self sendObject:object delegate:delegate block:^(RKObjectLoader* loader) { + +- (void)sendObject:(id)object method:(RKRequestMethod)method usingBlock:(void(^)(RKObjectLoader *))block { + NSString *resourcePath = [self.router resourcePathForObject:object method:method]; + [self sendObject:object toResourcePath:resourcePath usingBlock:^(RKObjectLoader *loader) { loader.method = method; block(loader); }]; } -- (RKObjectLoader*)getObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - return [self sendObject:object method:RKRequestMethodGET delegate:delegate block:block]; +- (void)getObject:(id)object usingBlock:(void(^)(RKObjectLoader *))block { + [self sendObject:object method:RKRequestMethodGET usingBlock:block]; } -- (RKObjectLoader*)postObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - return [self sendObject:object method:RKRequestMethodPOST delegate:delegate block:block]; +- (void)postObject:(id)object usingBlock:(void(^)(RKObjectLoader *))block { + [self sendObject:object method:RKRequestMethodPOST usingBlock:block]; } -- (RKObjectLoader*)putObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - return [self sendObject:object method:RKRequestMethodPUT delegate:delegate block:block]; +- (void)putObject:(id)object usingBlock:(void(^)(RKObjectLoader *))block { + [self sendObject:object method:RKRequestMethodPUT usingBlock:block]; } -- (RKObjectLoader*)deleteObject:(id)object delegate:(id)delegate block:(void(^)(RKObjectLoader*))block { - return [self sendObject:object method:RKRequestMethodDELETE delegate:delegate block:block]; +- (void)deleteObject:(id)object usingBlock:(void(^)(RKObjectLoader *))block { + [self sendObject:object method:RKRequestMethodDELETE usingBlock:block]; } #endif // NS_BLOCKS_AVAILABLE #pragma mark - Object Instance Loaders for Non-nested JSON -- (RKObjectLoader*)getObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodGET delegate:delegate]; - if ([object isMemberOfClass:[objectMapping objectClass]]) { - loader.targetObject = object; - } else { - loader.targetObject = nil; - } - loader.objectMapping = objectMapping; - [loader send]; - return loader; +- (void)getObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { + [self sendObject:object method:RKRequestMethodGET usingBlock:^(RKObjectLoader *loader) { + loader.delegate = delegate; + loader.objectMapping = objectMapping; + }]; } -- (RKObjectLoader*)postObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodPOST delegate:delegate]; - if ([object isMemberOfClass:[objectMapping objectClass]]) { - loader.targetObject = object; - } else { - loader.targetObject = nil; - } - loader.objectMapping = objectMapping; - [loader send]; - return loader; +- (void)postObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { + [self sendObject:object method:RKRequestMethodPOST usingBlock:^(RKObjectLoader *loader) { + loader.delegate = delegate; + loader.objectMapping = objectMapping; + }]; } -- (RKObjectLoader*)putObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodPUT delegate:delegate]; - if ([object isMemberOfClass:[objectMapping objectClass]]) { - loader.targetObject = object; - } else { - loader.targetObject = nil; - } - loader.objectMapping = objectMapping; - [loader send]; - return loader; +- (void)putObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { + [self sendObject:object method:RKRequestMethodPUT usingBlock:^(RKObjectLoader *loader) { + loader.delegate = delegate; + loader.objectMapping = objectMapping; + }]; } -- (RKObjectLoader*)deleteObject:(id)object mapResponseWith:(RKObjectMapping*)objectMapping delegate:(id)delegate { - RKObjectLoader* loader = [self objectLoaderForObject:object method:RKRequestMethodDELETE delegate:delegate]; - if ([object isMemberOfClass:[objectMapping objectClass]]) { - loader.targetObject = object; - } else { - loader.targetObject = nil; - } - loader.objectMapping = objectMapping; - [loader send]; - return loader; +- (void)deleteObject:(id)object mapResponseWith:(RKObjectMapping *)objectMapping delegate:(id)delegate { + [self sendObject:object method:RKRequestMethodDELETE usingBlock:^(RKObjectLoader *loader) { + loader.delegate = delegate; + loader.objectMapping = objectMapping; + }]; } - (RKRequestCache *)requestCache { @@ -338,4 +388,59 @@ - (RKRequestQueue *)requestQueue { return self.client.requestQueue; } +- (void)setMappingQueue:(dispatch_queue_t)newMappingQueue { + if (_mappingQueue) { + dispatch_release(_mappingQueue); + _mappingQueue = nil; + } + + if (newMappingQueue) { + dispatch_retain(newMappingQueue); + _mappingQueue = newMappingQueue; + } +} + +#pragma mark - RKConfigrationDelegate + +- (void)configureRequest:(RKRequest *)request { + [self.client configureRequest:request]; +} + +- (void)configureObjectLoader:(RKObjectLoader *)objectLoader { + objectLoader.serializationMIMEType = self.serializationMIMEType; + [self configureRequest:objectLoader]; +} + +#pragma mark - Deprecations + ++ (RKObjectManager *)objectManagerWithBaseURLString:(NSString *)baseURLString { + return [self managerWithBaseURLString:baseURLString]; +} + ++ (RKObjectManager *)objectManagerWithBaseURL:(NSURL *)baseURL { + return [self managerWithBaseURL:baseURL]; +} + +- (RKObjectLoader *)objectLoaderWithResourcePath:(NSString *)resourcePath delegate:(id)delegate { + RKObjectLoader* loader = [self loaderWithResourcePath:resourcePath]; + loader.delegate = delegate; + + return loader; +} + +- (RKObjectLoader*)objectLoaderForObject:(id)object method:(RKRequestMethod)method delegate:(id)delegate { + RKObjectLoader *loader = [self loaderForObject:object method:method]; + loader.delegate = delegate; + return loader; +} + +- (void)loadObjectsAtResourcePath:(NSString *)resourcePath objectMapping:(RKObjectMapping *)objectMapping delegate:(id)delegate { + RKObjectLoader *loader = [self loaderWithResourcePath:resourcePath]; + loader.delegate = delegate; + loader.method = RKRequestMethodGET; + loader.objectMapping = objectMapping; + + [loader send]; +} + @end diff --git a/Code/ObjectMapping/RKObjectMapper.h b/Code/ObjectMapping/RKObjectMapper.h index 27833e2917..d7fc361c6c 100644 --- a/Code/ObjectMapping/RKObjectMapper.h +++ b/Code/ObjectMapping/RKObjectMapper.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/6/11. -// Copyright 2011 Two Toasters -// +// 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,10 +26,6 @@ #import "RKMappingOperationQueue.h" #import "Support.h" -/** - Maps parsed primitive dictionary and arrays into objects. This is the primary entry point - for an external object mapping operation. - */ @class RKObjectMapper; @protocol RKObjectMapperDelegate @@ -39,26 +35,27 @@ - (void)objectMapperWillBeginMapping:(RKObjectMapper*)objectMapper; - (void)objectMapperDidFinishMapping:(RKObjectMapper*)objectMapper; - (void)objectMapper:(RKObjectMapper*)objectMapper didAddError:(NSError*)error; -- (void)objectMapper:(RKObjectMapper*)objectMapper didFindMappableObject:(id)object atKeyPath:(NSString*)keyPath withMapping:(id)mapping; +- (void)objectMapper:(RKObjectMapper*)objectMapper didFindMappableObject:(id)object atKeyPath:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)mapping; - (void)objectMapper:(RKObjectMapper*)objectMapper didNotFindMappableObjectAtKeyPath:(NSString*)keyPath; -- (void)objectMapper:(RKObjectMapper*)objectMapper willMapFromObject:(id)sourceObject toObject:(id)destinationObject atKeyPath:(NSString*)keyPath usingMapping:(id)objectMapping; -- (void)objectMapper:(RKObjectMapper*)objectMapper didMapFromObject:(id)sourceObject toObject:(id)destinationObject atKeyPath:(NSString*)keyPath usingMapping:(id)objectMapping; -- (void)objectMapper:(RKObjectMapper*)objectMapper didFailMappingFromObject:(id)sourceObject toObject:(id)destinationObject withError:(NSError*)error atKeyPath:(NSString*)keyPath usingMapping:(id)objectMapping; +- (void)objectMapper:(RKObjectMapper*)objectMapper willMapFromObject:(id)sourceObject toObject:(id)destinationObject atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMappingDefinition *)objectMapping; +- (void)objectMapper:(RKObjectMapper*)objectMapper didMapFromObject:(id)sourceObject toObject:(id)destinationObject atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMappingDefinition *)objectMapping; +- (void)objectMapper:(RKObjectMapper*)objectMapper didFailMappingFromObject:(id)sourceObject toObject:(id)destinationObject withError:(NSError*)error atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMappingDefinition *)objectMapping; @end +/** + + */ @interface RKObjectMapper : NSObject { - id _sourceObject; - id _targetObject; - RKObjectMappingProvider* _mappingProvider; - id _delegate; - NSMutableArray* _errors; - RKMappingOperationQueue *_operationQueue; + @protected + RKMappingOperationQueue *operationQueue; + NSMutableArray* errors; } @property (nonatomic, readonly) id sourceObject; @property (nonatomic, assign) id targetObject; @property (nonatomic, readonly) RKObjectMappingProvider* mappingProvider; +@property (nonatomic, assign) RKObjectMappingProviderContext context; @property (nonatomic, assign) id delegate; @property (nonatomic, readonly) NSArray* errors; diff --git a/Code/ObjectMapping/RKObjectMapper.m b/Code/ObjectMapping/RKObjectMapper.m index 473c3fca7a..cc97b1b92d 100644 --- a/Code/ObjectMapping/RKObjectMapper.m +++ b/Code/ObjectMapping/RKObjectMapper.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/6/11. -// Copyright 2011 Two Toasters -// +// 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,6 +21,7 @@ #import "RKObjectMapper.h" #import "RKObjectMapperError.h" #import "RKObjectMapper_Private.h" +#import "RKObjectMappingProvider+Contexts.h" // Set Logging Component #undef RKLogComponent @@ -28,114 +29,120 @@ @implementation RKObjectMapper -@synthesize sourceObject = _sourceObject; -@synthesize targetObject = _targetObject; -@synthesize delegate =_delegate; -@synthesize mappingProvider = _mappingProvider; -@synthesize errors = _errors; +@synthesize sourceObject; +@synthesize targetObject; +@synthesize delegate; +@synthesize mappingProvider; +@synthesize errors; +@synthesize context; -+ (id)mapperWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider { - return [[[self alloc] initWithObject:object mappingProvider:mappingProvider] autorelease]; ++ (id)mapperWithObject:(id)object mappingProvider:(RKObjectMappingProvider *)theMappingProvider { + return [[[self alloc] initWithObject:object mappingProvider:theMappingProvider] autorelease]; } -- (id)initWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider { +- (id)initWithObject:(id)object mappingProvider:(RKObjectMappingProvider *)theMappingProvider { self = [super init]; if (self) { - _sourceObject = [object retain]; - _mappingProvider = mappingProvider; - _errors = [NSMutableArray new]; - _operationQueue = [RKMappingOperationQueue new]; + sourceObject = [object retain]; + mappingProvider = theMappingProvider; + errors = [NSMutableArray new]; + operationQueue = [RKMappingOperationQueue new]; + context = RKObjectMappingProviderContextObjectsByKeyPath; } - + return self; } - (void)dealloc { - [_sourceObject release]; - [_errors release]; - [_operationQueue release]; + [sourceObject release]; + [errors release]; + [operationQueue release]; [super dealloc]; } #pragma mark - Errors +- (NSArray *)errors { + return [NSArray arrayWithArray:errors]; +} + - (NSUInteger)errorCount { return [self.errors count]; } -- (void)addError:(NSError*)error { +- (void)addError:(NSError *)error { NSAssert(error, @"Cannot add a nil error"); - [_errors addObject:error]; - + [errors addObject:error]; + if ([self.delegate respondsToSelector:@selector(objectMapper:didAddError:)]) { [self.delegate objectMapper:self didAddError:error]; } - + RKLogWarning(@"Adding mapping error: %@", [error localizedDescription]); } -- (void)addErrorWithCode:(RKObjectMapperErrorCode)errorCode message:(NSString*)errorMessage keyPath:(NSString*)keyPath userInfo:(NSDictionary*)otherInfo { - NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: +- (void)addErrorWithCode:(RKObjectMapperErrorCode)errorCode message:(NSString *)errorMessage keyPath:(NSString *)keyPath userInfo:(NSDictionary *)otherInfo { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: errorMessage, NSLocalizedDescriptionKey, - @"RKObjectMapperKeyPath", keyPath ? keyPath : (NSString*) [NSNull null], + @"RKObjectMapperKeyPath", keyPath ? keyPath : (NSString *) [NSNull null], nil]; [userInfo addEntriesFromDictionary:otherInfo]; - NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:errorCode userInfo:userInfo]; + NSError* error = [NSError errorWithDomain:RKErrorDomain code:errorCode userInfo:userInfo]; [self addError:error]; } -- (void)addErrorForUnmappableKeyPath:(NSString*)keyPath { - NSString* errorMessage = [NSString stringWithFormat:@"Could not find an object mapping for keyPath: '%@'", keyPath]; +- (void)addErrorForUnmappableKeyPath:(NSString *)keyPath { + NSString *errorMessage = [NSString stringWithFormat:@"Could not find an object mapping for keyPath: '%@'", keyPath]; [self addErrorWithCode:RKObjectMapperErrorObjectMappingNotFound message:errorMessage keyPath:keyPath userInfo:nil]; } - (BOOL)isNullCollection:(id)object { // The purpose of this method is to guard against the case where we perform valueForKeyPath: on an array - // and it returns NSNull for each element in the array. - + // and it returns NSNull for each element in the array. + // We consider an empty array/dictionary mappable, but a collection that contains only NSNull // values is unmappable if ([object respondsToSelector:@selector(objectForKey:)]) { return NO; } - - if ([object respondsToSelector:@selector(countForObject:)] && [object count] > 0) { + + if ([object respondsToSelector:@selector(countForObject:)] && [object count] > 0) { if ([object countForObject:[NSNull null]] == [object count]) { RKLogDebug(@"Found a collection containing only NSNull values, considering the collection unmappable..."); return YES; } } - + return NO; } #pragma mark - Mapping Primitives -- (id)mapObject:(id)mappableObject atKeyPath:(NSString*)keyPath usingMapping:(id)mapping { +- (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(RKObjectMappingDefinition *)mapping { NSAssert([mappableObject respondsToSelector:@selector(setValue:forKeyPath:)], @"Expected self.object to be KVC compliant"); id destinationObject = nil; - + if (self.targetObject) { destinationObject = self.targetObject; - RKObjectMapping* objectMapping = nil; + RKObjectMapping *objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { - objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:mappableObject]; + objectMapping = [(RKDynamicObjectMapping *)mapping objectMappingForDictionary:mappableObject]; } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { - objectMapping = (RKObjectMapping*)mapping; + 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 '%@'", - NSStringFromClass([self.targetObject class]), NSStringFromClass(objectMapping.objectClass)]; + NSString *errorMessage = [NSString stringWithFormat: + @"Expected an object mapping for class of type '%@', provider returned one for '%@'", + NSStringFromClass([self.targetObject class]), NSStringFromClass(objectMapping.objectClass)]; [self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil]; return nil; } } else { destinationObject = [self objectWithMapping:mapping andData:mappableObject]; } - + if (mapping && destinationObject) { BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { @@ -146,72 +153,72 @@ - (id)mapObject:(id)mappableObject atKeyPath:(NSString*)keyPath usingMapping:(id [self addErrorForUnmappableKeyPath:keyPath]; return nil; } - + return nil; } -- (NSArray*)mapCollection:(NSArray*)mappableObjects atKeyPath:(NSString*)keyPath usingMapping:(id)mapping { +- (NSArray *)mapCollection:(NSArray *)mappableObjects atKeyPath:(NSString *)keyPath usingMapping:(RKObjectMappingDefinition *)mapping { NSAssert(mappableObjects != nil, @"Cannot map without an collection of mappable objects"); NSAssert(mapping != nil, @"Cannot map without a mapping to consult"); - - NSArray* objectsToMap = mappableObjects; + + NSArray *objectsToMap = mappableObjects; if (mapping.forceCollectionMapping) { // If we have forced mapping of a dictionary, map each subdictionary if ([mappableObjects isKindOfClass:[NSDictionary class]]) { RKLogDebug(@"Collection mapping forced for NSDictionary, mapping each key/value independently..."); objectsToMap = [NSMutableArray arrayWithCapacity:[mappableObjects count]]; for (id key in mappableObjects) { - NSDictionary* dictionaryToMap = [NSDictionary dictionaryWithObject:[mappableObjects valueForKey:key] forKey:key]; - [(NSMutableArray*)objectsToMap addObject:dictionaryToMap]; + NSDictionary *dictionaryToMap = [NSDictionary dictionaryWithObject:[mappableObjects valueForKey:key] forKey:key]; + [(NSMutableArray *)objectsToMap addObject:dictionaryToMap]; } } else { RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([mappableObjects class])); } } - + // Ensure we are mapping onto a mutable collection if there is a target - NSMutableArray* mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[mappableObjects count]]; + NSMutableArray *mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[mappableObjects count]]; if (NO == [mappedObjects respondsToSelector:@selector(addObject:)]) { - NSString* errorMessage = [NSString stringWithFormat: - @"Cannot map a collection of objects onto a non-mutable collection. Unexpected destination object type '%@'", - NSStringFromClass([mappedObjects class])]; + NSString *errorMessage = [NSString stringWithFormat: + @"Cannot map a collection of objects onto a non-mutable collection. Unexpected destination object type '%@'", + NSStringFromClass([mappedObjects class])]; [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]; } } - + return mappedObjects; } // The workhorse of this entire process. Emits object loading operations -- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:keyPath usingMapping:(id)mapping { - NSAssert(destinationObject != nil, @"Cannot map without a target object to assign the results to"); +- (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(mappableObject != nil, @"Cannot map without a collection of attributes"); NSAssert(mapping != nil, @"Cannot map without an mapping"); - + RKLogDebug(@"Asked to map source object %@ with mapping %@", mappableObject, mapping); 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]; - operation.queue = _operationQueue; - BOOL success = [operation performMapping:&error]; + + NSError *error = nil; + + RKObjectMappingOperation *operation = [RKObjectMappingOperation mappingOperationFromObject:mappableObject + toObject:destinationObject + withMapping:mapping]; + operation.queue = operationQueue; + 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]; @@ -222,96 +229,132 @@ - (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPat } [self addError:error]; } - + + return success; } -- (id)objectWithMapping:(id)mapping andData:(id)mappableData { - NSAssert([mapping conformsToProtocol:@protocol(RKObjectMappingDefinition)], @"Expected an object implementing RKObjectMappingDefinition"); - RKObjectMapping* objectMapping = nil; +- (id)objectWithMapping:(RKObjectMappingDefinition *)mapping andData:(id)mappableData { + NSAssert([mapping isKindOfClass:[RKObjectMappingDefinition class]], @"Expected an RKObjectMappingDefinition object"); + RKObjectMapping *objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { - objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:mappableData]; + objectMapping = [(RKDynamicObjectMapping *)mapping objectMappingForDictionary:mappableData]; if (! objectMapping) { RKLogDebug(@"Mapping %@ declined mapping for data %@: returned nil objectMapping", mapping, mappableData); } } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { - objectMapping = (RKObjectMapping*)mapping; + objectMapping = (RKObjectMapping *)mapping; } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); } - + if (objectMapping) { return [objectMapping mappableObjectForData:mappableData]; } - + return nil; } -// 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]; +- (id)performMappingForObject:(id)mappableValue atKeyPath:(NSString *)keyPath usingMapping:(RKObjectMappingDefinition *)mapping { + id mappingResult; + if (mapping.forceCollectionMapping || [mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) { + RKLogDebug(@"Found mappable collection at keyPath '%@': %@", keyPath, mappableValue); + mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:mapping]; + } else { + RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue); + mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping:mapping]; } - - // Perform the mapping + + return mappingResult; +} + +- (NSMutableDictionary *)performKeyPathMappingUsingMappingDictionary:(NSDictionary *)mappingsByKeyPath { BOOL foundMappable = NO; - NSMutableDictionary* results = [NSMutableDictionary dictionary]; - NSDictionary* mappingsByKeyPath = [self.mappingProvider mappingsByKeyPath]; - for (NSString* keyPath in mappingsByKeyPath) { - id mappingResult; - id mappableValue; - + NSMutableDictionary *results = [NSMutableDictionary dictionary]; + for (NSString *keyPath in mappingsByKeyPath) { + id mappingResult = nil; + id mappableValue = nil; + RKLogTrace(@"Examining keyPath '%@' for mappable content...", keyPath); - + if ([keyPath isEqualToString:@""]) { mappableValue = self.sourceObject; } else { mappableValue = [self.sourceObject valueForKeyPath:keyPath]; } - + // Not found... if (mappableValue == nil || mappableValue == [NSNull null] || [self isNullCollection:mappableValue]) { RKLogDebug(@"Found unmappable value at keyPath: %@", keyPath); - + if ([self.delegate respondsToSelector:@selector(objectMapper:didNotFindMappableObjectAtKeyPath:)]) { [self.delegate objectMapper:self didNotFindMappableObjectAtKeyPath:keyPath]; } - + continue; } - + // Found something to map foundMappable = YES; - id mapping = [mappingsByKeyPath objectForKey:keyPath]; + RKObjectMappingDefinition * mapping = [mappingsByKeyPath objectForKey:keyPath]; if ([self.delegate respondsToSelector:@selector(objectMapper:didFindMappableObject:atKeyPath:withMapping:)]) { [self.delegate objectMapper:self didFindMappableObject:mappableValue atKeyPath:keyPath withMapping:mapping]; } - if (mapping.forceCollectionMapping || [mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) { - RKLogDebug(@"Found mappable collection at keyPath '%@': %@", keyPath, mappableValue); - mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:mapping]; - } else { - RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue); - mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping: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. +- (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]]) { + results = [self performKeyPathMappingUsingMappingDictionary:mappingsForContext]; + foundMappable = (results != nil); + } else if ([mappingsForContext isKindOfClass:[RKObjectMappingDefinition class]]) { + id mappableData = self.sourceObject; + if ([mappingsForContext rootKeyPath] != nil) { + NSString* rootKeyPath = [mappingsForContext rootKeyPath]; + 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; + results = [NSDictionary dictionaryWithObject:mappingResult forKey:@""]; + } + } + // Allow any queued operations to complete - RKLogDebug(@"The following operations are in the queue: %@", _operationQueue.operations); - [_operationQueue waitUntilAllOperationsAreFinished]; - + RKLogDebug(@"The following operations are in the queue: %@", operationQueue.operations); + [operationQueue waitUntilAllOperationsAreFinished]; + if ([self.delegate respondsToSelector:@selector(objectMapperDidFinishMapping:)]) { [self.delegate objectMapperDidFinishMapping:self]; } - + // If we found nothing eligible for mapping in the content, add an unmappable key path error and fail mapping // If the content is empty, we don't consider it an error BOOL isEmpty = [self.sourceObject respondsToSelector:@selector(count)] && ([self.sourceObject count] == 0); @@ -319,9 +362,9 @@ - (RKObjectMappingResult*)performMapping { [self addErrorForUnmappableKeyPath:@""]; return nil; } - + RKLogDebug(@"Finished performing object mapping. Results: %@", results); - + return [RKObjectMappingResult mappingResultWithDictionary:results]; } diff --git a/Code/ObjectMapping/RKObjectMapperError.h b/Code/ObjectMapping/RKObjectMapperError.h index d4586f0a0f..bfe29eafa1 100644 --- a/Code/ObjectMapping/RKObjectMapperError.h +++ b/Code/ObjectMapping/RKObjectMapperError.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,9 +18,9 @@ // limitations under the License. // -#import "Errors.h" +#import "RKErrors.h" -typedef enum RKObjectMapperErrors { +typedef enum { RKObjectMapperErrorObjectMappingNotFound = 1001, // No mapping found RKObjectMapperErrorObjectMappingTypeMismatch = 1002, // Target class and object mapping are in disagreement RKObjectMapperErrorUnmappableContent = 1003, // No mappable attributes or relationships were found diff --git a/Code/ObjectMapping/RKObjectMapper_Private.h b/Code/ObjectMapping/RKObjectMapper_Private.h index 21c066de39..d78f2f86f4 100644 --- a/Code/ObjectMapping/RKObjectMapper_Private.h +++ b/Code/ObjectMapping/RKObjectMapper_Private.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/9/11. -// Copyright 2011 Two Toasters -// +// 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,9 +20,9 @@ @interface RKObjectMapper (Private) -- (id)mapObject:(id)mappableObject atKeyPath:keyPath usingMapping:(id)mapping; -- (NSArray*)mapCollection:(NSArray*)mappableObjects atKeyPath:(NSString*)keyPath usingMapping:(id)mapping; -- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:keyPath usingMapping:(id)mapping; -- (id)objectWithMapping:(id)objectMapping andData:(id)mappableData; +- (id)mapObject:(id)mappableObject atKeyPath:(NSString *)keyPath usingMapping:(RKObjectMappingDefinition *)mapping; +- (NSArray*)mapCollection:(NSArray*)mappableObjects atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMappingDefinition *)mapping; +- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:(NSString *)keyPath usingMapping:(RKObjectMappingDefinition *)mapping; +- (id)objectWithMapping:(RKObjectMappingDefinition *)objectMapping andData:(id)mappableData; @end diff --git a/Code/ObjectMapping/RKObjectMapping.h b/Code/ObjectMapping/RKObjectMapping.h index d15dd793af..7c0f51f557 100644 --- a/Code/ObjectMapping/RKObjectMapping.h +++ b/Code/ObjectMapping/RKObjectMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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,31 +28,30 @@ compliant object into another representation. The mapping is defined in terms of a source object class and a collection of rules defining how keyPaths should be transformed into target attributes and relationships. - + There are two types of transformations possible: - + 1. keyPath to attribute. Defines that the value found at the keyPath should be transformed and assigned to the property specified by the attribute. The transformation to be performed is determined by inspecting the type of the target property at runtime. 1. keyPath to relationship. Defines that the value found at the keyPath should be -transformed into another object instance and assigned to the property specified by the +transformed into another object instance and assigned to the property specified by the relationship. Relationships are processed using an object mapping as well. - + Through the use of relationship mappings, an arbitrarily complex object graph can be mapped for you. - + Instances of RKObjectMapping are used to configure RKObjectMappingOperation instances, which actually perform the mapping work. Both object loading and serialization are defined in terms of object mappings. */ -@interface RKObjectMapping : NSObject { +@interface RKObjectMapping : RKObjectMappingDefinition { Class _objectClass; - NSMutableArray *_mappings; - NSString *_rootKeyPath; + NSMutableArray* _mappings; + NSString* _rootKeyPath; BOOL _setDefaultValueForMissingAttributes; BOOL _setNilForMissingRelationships; - BOOL _forceCollectionMapping; BOOL _performKeyValueValidation; NSArray *_dateFormatters; - NSDateFormatter *_preferredDateFormatter; + NSFormatter *_preferredDateFormatter; } /** @@ -60,35 +59,33 @@ relationship. Relationships are processed using an object mapping as well. */ @property (nonatomic, assign) Class objectClass; +/** + The name of the target class the receiver defines a mapping for. + + @see objectClass + */ +@property (nonatomic, copy) NSString *objectClassName; + /** The aggregate collection of attribute and relationship mappings within this object mapping */ -@property (nonatomic, readonly) NSArray* mappings; +@property (nonatomic, readonly) NSArray *mappings; /** The collection of attribute mappings within this object mapping */ -@property (nonatomic, readonly) NSArray* attributeMappings; +@property (nonatomic, readonly) NSArray *attributeMappings; /** The collection of relationship mappings within this object mapping */ -@property (nonatomic, readonly) NSArray* relationshipMappings; +@property (nonatomic, readonly) NSArray *relationshipMappings; /** The collection of mappable keyPaths that are defined within this object mapping. These keyPaths refer to keys within the source object being mapped (i.e. the parsed JSON payload). */ -@property (nonatomic, readonly) NSArray* mappedKeyPaths; - -/** - The root keyPath for this object. When the object mapping is being used for serialization - and a root keyPath has been defined, the serialized object will be nested under this root keyPath - before being encoded for transmission to a remote system. - - @see RKObjectSerializer - */ -@property (nonatomic, retain) NSString* rootKeyPath; +@property (nonatomic, readonly) NSArray *mappedKeyPaths; /** When YES, any attributes that have mappings defined but are not present within the source @@ -103,69 +100,67 @@ 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: */ @property (nonatomic, assign) BOOL performKeyValueValidation; /** - 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 - */ -@property (nonatomic, assign) BOOL forceCollectionMapping; + When YES, RestKit will check that the object being mapped is key-value coding + compliant for the mapped key. If it is not, the attribute/relationship mapping will + be ignored and mapping will continue. When NO, unknown keyPath mappings will generate + NSUnknownKeyException errors for the unknown keyPath. + + Defaults to NO to help the developer catch incorrect mapping configurations during + development. + **Default**: NO + */ +@property (nonatomic, assign) BOOL ignoreUnknownKeyPaths; /** An array of NSDateFormatter objects to use when mapping string values 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; /** - The NSDateFormatter instance for your application's preferred date + The NSFormatter object for your application's preferred date 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) NSDateFormatter *preferredDateFormatter; +@property (nonatomic, retain) NSFormatter *preferredDateFormatter; + +#pragma mark - Mapping Instantiation /** Returns an object mapping for the specified class that is ready for configuration */ + (id)mappingForClass:(Class)objectClass; +/** + Creates and returns an object mapping for the class with the given name + + @param objectClassName The name of the class the mapping is for. + @return A new object mapping with for the class with given name. + */ ++ (id)mappingForClassWithName:(NSString *)objectClassName; + /** Returns an object mapping useful for configuring a serialization mapping. The object class is configured as NSMutableDictionary @@ -177,91 +172,110 @@ 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 delegate:self block:^(RKObjectLoader* loader) { - loader.objectMapping = [RKObjectMapping mappingForClass:[Person class] block:^(RKObjectMapping* mapping) { + + [[RKObjectManager sharedManager] postObject:self usingBlock:^(RKObjectLoader* loader) { + loader.objectMapping = [RKObjectMapping mappingForClass:[Person class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"email", @"first_name", nil]; }]; }]; */ -+ (id)mappingForClass:(Class)objectClass block:(void(^)(RKObjectMapping*))block; ++ (id)mappingForClass:(Class)objectClass usingBlock:(void (^)(RKObjectMapping *mapping))block; /** 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; [[RKObjectManager sharedManager] putObject:self delegate:self block:^(RKObjectLoader* loader) { - loader.serializationMapping = [RKObjectMapping serializationMappingWithBlock:^(RKObjectMapping* mapping) { + loader.serializationMapping = [RKObjectMapping serializationMappingUsingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"password", nil]; }]; }]; } } - + Using the block forms we are able to quickly configure and send this request on the fly. */ -+ (id)serializationMappingWithBlock:(void(^)(RKObjectMapping*))block; ++ (id)serializationMappingUsingBlock:(void (^)(RKObjectMapping *serializationMapping))block; #endif /** Add a configured attribute mapping to this object mapping - + @see RKObjectAttributeMapping */ - (void)addAttributeMapping:(RKObjectAttributeMapping*)mapping; /** Add a configured attribute mapping to this object mapping - + @see RKObjectRelationshipMapping */ - (void)addRelationshipMapping:(RKObjectRelationshipMapping*)mapping; +#pragma mark - Retrieving Mappings + /** Returns the attribute or relationship mapping for the given source keyPath. - - @param sourceKeyPath A keyPath within the mappable source object that is mapped to an + + @param sourceKeyPath A keyPath within the mappable source object that is mapped to an attribute or relationship in this object mapping. */ - (id)mappingForKeyPath:(NSString*)sourceKeyPath; +/** + Returns the attribute or relationship mapping for the given source keyPath. + + @param sourceKeyPath A keyPath within the mappable source object that is mapped to an + attribute or relationship in this object mapping. + */ +- (id)mappingForSourceKeyPath:(NSString*)sourceKeyPath; + +/** + 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; + /** Returns the attribute mapping targeting the specified attribute on the destination object - + @param attributeKey The name of the attribute we want to retrieve the mapping for */ - (RKObjectAttributeMapping *)mappingForAttribute:(NSString *)attributeKey; /** Returns the relationship mapping targeting the specified relationship on the destination object - + @param relationshipKey The name of the relationship we want to retrieve the mapping for */ - (RKObjectRelationshipMapping*)mappingForRelationship:(NSString*)relationshipKey; +#pragma mark - Attribute & Relationship Mapping + /** Define an attribute mapping for one or more keyPaths where the source keyPath and 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 mapAttributes:@"name", @"age", nil]; - - @param attributeKey A key-value coding key corresponding to a value in the mappable source object and an attribute + + @param attributeKey A key-value coding key corresponding to a value in the mappable source object and an attribute on the destination class that have the same name. */ - (void)mapAttributes:(NSString *)attributeKey, ... NS_REQUIRES_NIL_TERMINATION; @@ -269,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; @@ -285,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; @@ -301,52 +315,52 @@ relationship. Relationships are processed using an object mapping as well. /** Defines a relationship mapping for a key where the source keyPath and the destination relationship property have the same name. - + For example, given the transformation from a JSON dictionary: - + {"name": "My Name", "age": 28, "cat": { "name": "Asia" } } - + To a Person class with corresponding 'cat' relationship property, we could configure the mappings via: - + RKObjectMapping* catMapping = [RKObjectMapping mappingForClass:[Cat class]]; [personMapping mapRelationship:@"cat" withObjectMapping:catMapping]; - - @param relationshipKey A key-value coding key corresponding to a value in the mappable source object and a property + + @param relationshipKey A key-value coding key corresponding to a value in the mappable source object and a property on the destination class that have the same name. @param objectOrDynamicMapping An RKObjectMapping or RKObjectDynamic mapping to apply when mapping the relationship */ -- (void)mapRelationship:(NSString*)relationshipKey withMapping:(id)objectOrDynamicMapping; +- (void)mapRelationship:(NSString*)relationshipKey withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping; /** Syntactic sugar to improve readability when defining a relationship mapping. Implies that the mapping targets a one-to-many relationship nested within the source data. - + @see mapRelationship:withObjectMapping: */ -- (void)hasMany:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping; +- (void)hasMany:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping; /** Syntactic sugar to improve readability when defining a relationship mapping. Implies that the mapping targets a one-to-one relationship nested within the source data. - + @see mapRelationship:withObjectMapping: */ -- (void)hasOne:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping; +- (void)hasOne:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping; /** Instantiate and add an RKObjectAttributeMapping instance targeting a keyPath within the mappable - source data to an attribute on the target object. - + source data to an attribute on the target object. + Used to quickly define mappings where the source value is deeply nested in the mappable data or the source and destination do not have corresponding names. - + Examples: // We want to transform the name to something Cocoa-esque [mapping mapKeyPath:@"created_at" toAttribute:@"createdAt"]; - + // We want to extract nested data and map it to a property [mapping mapKeyPath:@"results.metadata.generated_on" toAttribute:@"generationTimestamp"]; - + @param sourceKeyPath A key-value coding keyPath to fetch the mappable value from @param destinationAttribute The attribute name to assign the mapped value to @see RKObjectAttributeMapping @@ -355,47 +369,47 @@ relationship. Relationships are processed using an object mapping as well. /** Instantiate and add an RKObjectRelationshipMapping instance targeting a keyPath within the mappable - source data to a relationship property on the target object. - + source data to a relationship property on the target object. + Used to quickly define mappings where the source value is deeply nested in the mappable data or the source and destination do not have corresponding names. - + Examples: // We want to transform the name to something Cocoa-esque [mapping mapKeyPath:@"best_friend" toRelationship:@"bestFriend" withObjectMapping:friendMapping]; - + // We want to extract nested data and map it to a property [mapping mapKeyPath:@"best_friend.favorite_cat" toRelationship:@"bestFriendsFavoriteCat" withObjectMapping:catMapping]; - + @param sourceKeyPath A key-value coding keyPath to fetch the mappable value from @param destinationRelationship The relationship name to assign the mapped value to @param objectMapping An object mapping to use when processing the nested objects @see RKObjectRelationshipMapping */ -- (void)mapKeyPath:(NSString *)sourceKeyPath toRelationship:(NSString*)destinationRelationship withMapping:(id)objectOrDynamicMapping; +- (void)mapKeyPath:(NSString *)sourceKeyPath toRelationship:(NSString*)destinationRelationship withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping; /** Instantiate and add an RKObjectRelationshipMapping instance targeting a keyPath within the mappable - source data to a relationship property on the target object. - + source data to a relationship property on the target object. + Used to indicate whether the relationship should be included in serialization. @param sourceKeyPath A key-value coding keyPath to fetch the mappable value from @param destinationRelationship The relationship name to assign the mapped value to @param objectMapping An object mapping to use when processing the nested objects @param serialize A boolean value indicating whether to include this relationship in serialization - + @see mapKeyPath:toRelationship:withObjectMapping: */ -- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping serialize:(BOOL)serialize; +- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping serialize:(BOOL)serialize; /** Quickly define a group of attribute mappings using alternating keyPath and attribute names. You must provide an equal number of keyPath and attribute pairs or an exception will be generated. - + For example: [personMapping mapKeyPathsToAttributes:@"name", @"name", @"createdAt", @"createdAt", @"street_address", @"streetAddress", nil]; - + @param sourceKeyPath A key-value coding key path to fetch a mappable value from @param ... A nil-terminated sequence of strings alternating between source key paths and destination attributes */ @@ -404,24 +418,24 @@ relationship. Relationships are processed using an object mapping as well. /** Configures a sub-key mapping for cases where JSON has been nested underneath a key named after an attribute. - + For example, consider the following JSON: - - { "users": - { + + { "users": + { "blake": { "id": 1234, "email": "blake@restkit.org" }, "rachit": { "id": 5678", "email": "rachit@restkit.org" } } } - + We can configure our mappings to handle this in the following form: - + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[User class]]; mapping.forceCollectionMapping = YES; // RestKit cannot infer this is a collection, so we force it [mapping mapKeyOfNestedDictionaryToAttribute:@"firstName"]; [mapping mapFromKeyPath:@"(firstName).id" toAttribute:"userID"]; [mapping mapFromKeyPath:@"(firstName).email" toAttribute:"email"]; - + [[RKObjectManager sharedManager].mappingProvider setObjectMapping:mapping forKeyPath:@"users"]; */ - (void)mapKeyOfNestedDictionaryToAttribute:(NSString *)attributeName; @@ -429,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 */ @@ -442,18 +456,20 @@ relationship. Relationships are processed using an object mapping as well. /** Removes an instance of an attribute or relationship mapping from the object mapping - + @param attributeOrRelationshipMapping The attribute or relationship mapping to remove */ - (void)removeMapping:(RKObjectAttributeMapping*)attributeOrRelationshipMapping; /** Remove the attribute or relationship mapping for the specified source keyPath - + @param sourceKeyPath A key-value coding key path to remove the mappings for */ - (void)removeMappingForKeyPath:(NSString*)sourceKeyPath; +#pragma mark - Inverse Mappings + /** Generates an inverse mapping for the rules specified within this object mapping. This can be used to quickly generate a corresponding serialization mapping from a configured object mapping. The inverse @@ -464,10 +480,10 @@ relationship. Relationships are processed using an object mapping as well. /** Returns the default value to be assigned to the specified attribute when it is missing from a mappable payload. - + The default implementation returns nil for transient object mappings. On managed object mappings, the default value returned from the Entity definition will be used. - + @see [RKManagedObjectMapping defaultValueForMissingAttribute:] */ - (id)defaultValueForMissingAttribute:(NSString*)attributeName; @@ -482,13 +498,26 @@ 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; +/** + Returns an auto-released object that can be used to apply this object mapping + given a set of mappable data. For transient objects, this generally returns an + instance of the objectClass. For Core Data backed persistent objects, mappableData + will be inspected to search for primary key data to lookup existing object instances. + */ +- (id)mappableObjectForData:(id)mappableData; + +// Deprecations ++ (id)mappingForClass:(Class)objectClass withBlock:(void (^)(RKObjectMapping*))block DEPRECATED_ATTRIBUTE; ++ (id)mappingForClass:(Class)objectClass block:(void (^)(RKObjectMapping*))block DEPRECATED_ATTRIBUTE; ++ (id)serializationMappingWithBlock:(void (^)(RKObjectMapping*))block DEPRECATED_ATTRIBUTE; + @end ///////////////////////////////////////////////////////////////////////////// @@ -505,13 +534,13 @@ 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 NSDateFormatter objects used when mapping strings into NSDate attributes + + @return An array of NSFormatter objects used when mapping strings into NSDate attributes */ + (NSArray *)defaultDateFormatters; @@ -519,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 */ @@ -527,16 +556,16 @@ relationship. Relationships are processed using an object mapping as well. /** Adds a date formatter instance to the default collection - - @param dateFormatter An NSDateFormatter object to append to the end of the default formatters collection + + @param dateFormatter An NSFormatter object to append to the end of the default formatters collection @see defaultDateFormatters */ -+ (void)addDefaultDateFormatter:(NSDateFormatter *)dateFormatter; ++ (void)addDefaultDateFormatter:(NSFormatter *)dateFormatter; /** 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 @@ -548,20 +577,20 @@ 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 NSDateFormatter to use when serializing dates into strings + + @return The preferred NSFormatter object to use when serializing dates into strings */ -+ (NSDateFormatter *)preferredDateFormatter; ++ (NSFormatter *)preferredDateFormatter; /** 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 NSDateFormatter to configured as the new preferred instance + + @param dateFormatter The NSFormatter object to designate as the new preferred instance */ -+ (void)setPreferredDateFormatter:(NSDateFormatter *)dateFormatter; ++ (void)setPreferredDateFormatter:(NSFormatter *)dateFormatter; @end diff --git a/Code/ObjectMapping/RKObjectMapping.m b/Code/ObjectMapping/RKObjectMapping.m index f1d43d6f56..373151f5c6 100644 --- a/Code/ObjectMapping/RKObjectMapping.m +++ b/Code/ObjectMapping/RKObjectMapping.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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,7 @@ #import "RKObjectRelationshipMapping.h" #import "RKObjectPropertyInspector.h" #import "RKLog.h" +#import "RKISO8601DateFormatter.h" // Constants NSString* const RKObjectMappingNestingAttributeKeyName = @""; @@ -35,28 +36,47 @@ @implementation RKObjectMapping @synthesize rootKeyPath = _rootKeyPath; @synthesize setDefaultValueForMissingAttributes = _setDefaultValueForMissingAttributes; @synthesize setNilForMissingRelationships = _setNilForMissingRelationships; -@synthesize forceCollectionMapping = _forceCollectionMapping; @synthesize performKeyValueValidation = _performKeyValueValidation; +@synthesize ignoreUnknownKeyPaths = _ignoreUnknownKeyPaths; + (id)mappingForClass:(Class)objectClass { RKObjectMapping* mapping = [self new]; - mapping.objectClass = objectClass; + mapping.objectClass = objectClass; return [mapping autorelease]; } ++ (id)mappingForClassWithName:(NSString *)objectClassName { + return [self mappingForClass:NSClassFromString(objectClassName)]; +} + + (id)serializationMapping { return [self mappingForClass:[NSMutableDictionary class]]; } #if NS_BLOCKS_AVAILABLE -+ (id)mappingForClass:(Class)objectClass block:(void(^)(RKObjectMapping*))block { ++ (id)mappingForClass:(Class)objectClass usingBlock:(void (^)(RKObjectMapping*))block { RKObjectMapping* mapping = [self mappingForClass:objectClass]; block(mapping); return mapping; } -+ (id)serializationMappingWithBlock:(void(^)(RKObjectMapping*))block { ++ (id)serializationMappingUsingBlock:(void (^)(RKObjectMapping*))block { + RKObjectMapping* mapping = [self serializationMapping]; + block(mapping); + return mapping; +} + +// Deprecated... Move to category or bottom... ++ (id)mappingForClass:(Class)objectClass withBlock:(void (^)(RKObjectMapping*))block { + return [self mappingForClass:objectClass usingBlock:block]; +} + ++ (id)mappingForClass:(Class)objectClass block:(void (^)(RKObjectMapping*))block { + return [self mappingForClass:objectClass usingBlock:block]; +} + ++ (id)serializationMappingWithBlock:(void (^)(RKObjectMapping*))block { RKObjectMapping* mapping = [self serializationMapping]; block(mapping); return mapping; @@ -72,11 +92,30 @@ - (id)init { self.setNilForMissingRelationships = NO; self.forceCollectionMapping = NO; self.performKeyValueValidation = YES; + self.ignoreUnknownKeyPaths = NO; } - + return self; } +- (id)copyWithZone:(NSZone *)zone { + RKObjectMapping *copy = [[[self class] allocWithZone:zone] init]; + copy.objectClass = self.objectClass; + copy.rootKeyPath = self.rootKeyPath; + copy.setDefaultValueForMissingAttributes = self.setDefaultValueForMissingAttributes; + copy.setNilForMissingRelationships = self.setNilForMissingRelationships; + copy.forceCollectionMapping = self.forceCollectionMapping; + copy.performKeyValueValidation = self.performKeyValueValidation; + copy.dateFormatters = self.dateFormatters; + copy.preferredDateFormatter = self.preferredDateFormatter; + + for (RKObjectAttributeMapping *mapping in self.mappings) { + [copy addAttributeMapping:mapping]; + } + + return copy; +} + - (void)dealloc { [_rootKeyPath release]; [_mappings release]; @@ -85,29 +124,37 @@ - (void)dealloc { [super dealloc]; } -- (NSArray*)mappedKeyPaths { +- (NSString *)objectClassName { + return NSStringFromClass(self.objectClass); +} + +- (void)setObjectClassName:(NSString *)objectClassName { + self.objectClass = NSClassFromString(objectClassName); +} + +- (NSArray *)mappedKeyPaths { return [_mappings valueForKey:@"destinationKeyPath"]; } -- (NSArray*)attributeMappings { +- (NSArray *)attributeMappings { NSMutableArray* mappings = [NSMutableArray array]; for (RKObjectAttributeMapping* mapping in self.mappings) { if ([mapping isMemberOfClass:[RKObjectAttributeMapping class]]) { [mappings addObject:mapping]; } } - + return mappings; } -- (NSArray*)relationshipMappings { +- (NSArray *)relationshipMappings { NSMutableArray* mappings = [NSMutableArray array]; for (RKObjectAttributeMapping* mapping in self.mappings) { if ([mapping isMemberOfClass:[RKObjectRelationshipMapping class]]) { [mappings addObject:mapping]; } } - + return mappings; } @@ -120,17 +167,31 @@ - (void)addRelationshipMapping:(RKObjectRelationshipMapping*)mapping { [self addAttributeMapping:mapping]; } -- (NSString*)description { - return [NSString stringWithFormat:@"RKObjectMapping class => %@: keyPath mappings => %@", NSStringFromClass(self.objectClass), _mappings]; +- (NSString *)description { + return [NSString stringWithFormat:@"<%@:%p objectClass=%@ keyPath mappings => %@>", NSStringFromClass([self class]), self, NSStringFromClass(self.objectClass), _mappings]; } -- (id)mappingForKeyPath:(NSString*)keyPath { +- (id)mappingForKeyPath:(NSString *)keyPath { + return [self mappingForSourceKeyPath:keyPath]; +} + +- (id)mappingForSourceKeyPath:(NSString *)sourceKeyPath { for (RKObjectAttributeMapping* mapping in _mappings) { - if ([mapping.sourceKeyPath isEqualToString:keyPath]) { + if ([mapping.sourceKeyPath isEqualToString:sourceKeyPath]) { return mapping; } } - + + return nil; +} + +- (id)mappingForDestinationKeyPath:(NSString *)destinationKeyPath { + for (RKObjectAttributeMapping* mapping in _mappings) { + if ([mapping.destinationKeyPath isEqualToString:destinationKeyPath]) { + return mapping; + } + } + return nil; } @@ -143,14 +204,14 @@ - (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]; } - + va_end(args); - + [self mapAttributesCollection:attributeKeyPaths]; } @@ -162,16 +223,16 @@ - (void)mapAttributesFromArray:(NSArray *)array { [self mapAttributesCollection:[NSSet setWithArray:array]]; } -- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping serialize:(BOOL)serialize { +- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping serialize:(BOOL)serialize { RKObjectRelationshipMapping* mapping = [RKObjectRelationshipMapping mappingFromKeyPath:relationshipKeyPath toKeyPath:keyPath withMapping:objectOrDynamicMapping reversible:serialize]; [self addRelationshipMapping:mapping]; } -- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping { +- (void)mapKeyPath:(NSString *)relationshipKeyPath toRelationship:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping { [self mapKeyPath:relationshipKeyPath toRelationship:keyPath withMapping:objectOrDynamicMapping serialize:YES]; } -- (void)mapRelationship:(NSString*)relationshipKeyPath withMapping:(id)objectOrDynamicMapping { +- (void)mapRelationship:(NSString*)relationshipKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping { [self mapKeyPath:relationshipKeyPath toRelationship:relationshipKeyPath withMapping:objectOrDynamicMapping]; } @@ -180,11 +241,11 @@ - (void)mapKeyPath:(NSString*)sourceKeyPath toAttribute:(NSString*)destinationKe [self addAttributeMapping:mapping]; } -- (void)hasMany:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping { +- (void)hasMany:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping { [self mapRelationship:keyPath withMapping:objectOrDynamicMapping]; } -- (void)hasOne:(NSString*)keyPath withMapping:(id)objectOrDynamicMapping { +- (void)hasOne:(NSString*)keyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping { [self mapRelationship:keyPath withMapping:objectOrDynamicMapping]; } @@ -210,10 +271,10 @@ - (RKObjectMapping*)inverseMappingAtDepth:(NSInteger)depth { for (RKObjectAttributeMapping* attributeMapping in self.attributeMappings) { [inverseMapping mapKeyPath:attributeMapping.destinationKeyPath toAttribute:attributeMapping.sourceKeyPath]; } - + for (RKObjectRelationshipMapping* relationshipMapping in self.relationshipMappings) { if (relationshipMapping.reversible) { - id mapping = relationshipMapping.mapping; + RKObjectMappingDefinition * mapping = relationshipMapping.mapping; if (! [mapping isKindOfClass:[RKObjectMapping class]]) { RKLogWarning(@"Unable to generate inverse mapping for relationship '%@': %@ relationships cannot be inversed.", relationshipMapping.sourceKeyPath, NSStringFromClass([mapping class])); continue; @@ -221,7 +282,7 @@ - (RKObjectMapping*)inverseMappingAtDepth:(NSInteger)depth { [inverseMapping mapKeyPath:relationshipMapping.destinationKeyPath toRelationship:relationshipMapping.sourceKeyPath withMapping:[(RKObjectMapping*)mapping inverseMappingAtDepth:depth+1]]; } } - + return inverseMapping; } @@ -233,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... @@ -241,7 +302,7 @@ - (void)mapKeyPathsToAttributes:(NSString*)firstKeyPath, ... { va_end(args); } -- (void)mapKeyOfNestedDictionaryToAttribute:(NSString*)attributeName { +- (void)mapKeyOfNestedDictionaryToAttribute:(NSString*)attributeName { [self mapKeyPath:RKObjectMappingNestingAttributeKeyName toAttribute:attributeName]; } @@ -255,7 +316,7 @@ - (RKObjectAttributeMapping*)mappingForAttribute:(NSString*)attributeKey { return mapping; } } - + return nil; } @@ -265,7 +326,7 @@ - (RKObjectRelationshipMapping*)mappingForRelationship:(NSString*)relationshipKe return mapping; } } - + return nil; } @@ -283,7 +344,7 @@ - (Class)classForProperty:(NSString*)propertyName { #pragma mark - Date and Time -- (NSDateFormatter *)preferredDateFormatter { +- (NSFormatter *)preferredDateFormatter { return _preferredDateFormatter ? _preferredDateFormatter : [RKObjectMapping preferredDateFormatter]; } @@ -303,12 +364,16 @@ @implementation RKObjectMapping (DateAndTimeFormatting) + (NSArray *)defaultDateFormatters { if (!defaultDateFormatters) { defaultDateFormatters = [[NSMutableArray alloc] initWithCapacity:2]; - + // Setup the default formatters - [self addDefaultDateFormatterForString:@"yyyy-MM-dd'T'HH:mm:ss'Z'" inTimeZone:nil]; + 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; } @@ -321,9 +386,9 @@ + (void)setDefaultDateFormatters:(NSArray *)dateFormatters { } -+ (void)addDefaultDateFormatter:(NSDateFormatter *)dateFormatter { ++ (void)addDefaultDateFormatter:(id)dateFormatter { [self defaultDateFormatters]; - [defaultDateFormatters addObject:dateFormatter]; + [defaultDateFormatters insertObject:dateFormatter atIndex:0]; } + (void)addDefaultDateFormatterForString:(NSString *)dateFormatString inTimeZone:(NSTimeZone *)nilOrTimeZone { @@ -335,13 +400,13 @@ + (void)addDefaultDateFormatterForString:(NSString *)dateFormatString inTimeZone } else { dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; } - + [self addDefaultDateFormatter:dateFormatter]; [dateFormatter release]; } -+ (NSDateFormatter *)preferredDateFormatter { ++ (NSFormatter *)preferredDateFormatter { if (!preferredDateFormatter) { // A date formatter that matches the output of [NSDate description] preferredDateFormatter = [NSDateFormatter new]; @@ -349,7 +414,7 @@ + (NSDateFormatter *)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 a8a02233be..aa67b3cfcd 100644 --- a/Code/ObjectMapping/RKObjectMappingDefinition.h +++ b/Code/ObjectMapping/RKObjectMappingDefinition.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/31/11. -// Copyright 2011 RestKit -// +// 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. @@ -18,8 +18,48 @@ // limitations under the License. // -@protocol RKObjectMappingDefinition +/** + 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; + +/** + 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. -- (BOOL)forceCollectionMapping; + @default NO + @see mapKeyOfNestedDictionaryToAttribute + */ +@property (nonatomic, assign) BOOL forceCollectionMapping; -@end \ No newline at end of file +@end diff --git a/Code/ObjectMapping/RKObjectMappingDefinition.m b/Code/ObjectMapping/RKObjectMappingDefinition.m new file mode 100644 index 0000000000..6bed9d1b25 --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingDefinition.m @@ -0,0 +1,16 @@ +// +// RKObjectMappingDefinition.m +// RestKit +// +// Created by Blake Watters on 2/15/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingDefinition.h" + +@implementation RKObjectMappingDefinition + +@synthesize rootKeyPath; +@synthesize forceCollectionMapping; + +@end diff --git a/Code/ObjectMapping/RKObjectMappingOperation.h b/Code/ObjectMapping/RKObjectMappingOperation.h index b1edaee0da..4535260ca0 100644 --- a/Code/ObjectMapping/RKObjectMappingOperation.h +++ b/Code/ObjectMapping/RKObjectMappingOperation.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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:(id)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:(id)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 16782c9788..711150e194 100644 --- a/Code/ObjectMapping/RKObjectMappingOperation.m +++ b/Code/ObjectMapping/RKObjectMappingOperation.m @@ -1,17 +1,16 @@ - // // RKObjectMappingOperation.m // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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 +24,7 @@ #import "RKObjectPropertyInspector.h" #import "RKObjectRelationshipMapping.h" #import "RKObjectMapper.h" -#import "Errors.h" +#import "RKErrors.h" #import "RKLog.h" // Set Logging Component @@ -37,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:); @@ -54,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] @@ -62,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; @@ -69,25 +73,27 @@ @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:(id)objectMapping { ++ (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]; } -- (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(id)objectMapping { +- (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(RKObjectMappingDefinition *)objectMapping { 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); @@ -95,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; } @@ -106,31 +112,54 @@ - (void)dealloc { [_objectMapping release]; [_nestedAttributeSubstitution release]; [_queue release]; - + [super dealloc]; } - (NSDate*)parseDateFromString:(NSString*)string { RKLogTrace(@"Transforming string value '%@' to NSDate...", string); - - NSDate* date = nil; - for (NSDateFormatter *dateFormatter in self.objectMapping.dateFormatters) { + + 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) { - date = [dateFormatter dateFromString:string]; + 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 (date) { - break; - } + + 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; } -- (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { +- (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)destinationType { 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 @@ -164,7 +193,7 @@ - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { } else if (orderedSetClass && [sourceType isSubclassOfClass:orderedSetClass]) { // OrderedSet -> Array if ([destinationType isSubclassOfClass:[NSArray class]]) { - return [(NSOrderedSet*)value array]; + return [value array]; } } else if ([sourceType isSubclassOfClass:[NSArray class]]) { // Array -> Set @@ -177,7 +206,12 @@ - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { } } else if ([sourceType isSubclassOfClass:[NSNumber class]] && [destinationType isSubclassOfClass:[NSDate class]]) { // Number -> Date - return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value intValue]]; + if ([destinationType isSubclassOfClass:[NSDate class]]) { + return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value intValue]]; + } else if ([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] && [destinationType isSubclassOfClass:[NSString class]]) { + return ([value boolValue] ? @"true" : @"false"); + } + return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value doubleValue]]; } else if ([sourceType isSubclassOfClass:[NSNumber class]] && [destinationType isSubclassOfClass:[NSDecimalNumber class]]) { // Number -> Decimal Number return [NSDecimalNumber decimalNumberWithDecimal:[value decimalValue]]; @@ -185,6 +219,11 @@ - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { [sourceType isSubclassOfClass:NSClassFromString(@"NSCFBoolean")] ) && [destinationType isSubclassOfClass:[NSString class]]) { return ([value boolValue] ? @"true" : @"false"); + if ([destinationType isSubclassOfClass:[NSDate class]]) { + return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value intValue]]; + } else if (([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] || [sourceType isSubclassOfClass:NSClassFromString(@"NSCFBoolean")]) && [destinationType isSubclassOfClass:[NSString class]]) { + return ([value boolValue] ? @"true" : @"false"); + } } else if ([destinationType isSubclassOfClass:[NSString class]] && [value respondsToSelector:@selector(stringValue)]) { return [value stringValue]; } else if ([destinationType isSubclassOfClass:[NSString class]] && [value isKindOfClass:[NSDate class]]) { @@ -192,64 +231,65 @@ - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { // Transform using the preferred date formatter NSString* dateString = nil; @synchronized(self.objectMapping.preferredDateFormatter) { - dateString = [self.objectMapping.preferredDateFormatter stringFromDate:value]; + dateString = [self.objectMapping.preferredDateFormatter stringForObjectValue:value]; } return dateString; } - + RKLogWarning(@"Failed transformation of value at keyPath '%@'. No strategy for transforming from '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType)); - + 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; - - if (self.objectMapping.performKeyValueValidation && [self.destinationObject respondsToSelector:@selector(validateValue:forKey:error:)]) { - success = [self.destinationObject validateValue:&value forKey:keyPath error:&_validationError]; - if (!success) { +- (BOOL)validateValue:(id *)value atKeyPath:(NSString*)keyPath { + 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); + RKLogWarning(@"Destination object %@ rejected attribute value %@ for keyPath %@. Skipping...", self.destinationObject, *value, keyPath); } } } - + return success; } -- (BOOL)shouldSetValue:(id)value atKeyPath:(NSString*)keyPath { +- (BOOL)shouldSetValue:(id *)value atKeyPath:(NSString*)keyPath { id currentValue = [self.destinationObject valueForKeyPath: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]]) { + if (*value == [NSNull null] || [*value isEqual:[NSNull null]]) { RKLogWarning(@"Coercing NSNull value to nil in shouldSetValue:atKeyPath: -- should be fixed."); - value = nil; + *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]) { + } + + if (! [self isValue:*value equalToValue:currentValue]) { // Validate value for key return [self validateValue:value atKeyPath:keyPath]; } @@ -268,10 +308,10 @@ - (NSArray*)applyNestingToMappings:(NSArray*)mappings { [array addObject:nestedMapping]; [nestedMapping release]; } - + return array; } - + return mappings; } @@ -288,23 +328,26 @@ - (void)applyAttributeMapping:(RKObjectAttributeMapping*)attributeMapping withVa [self.delegate objectMappingOperation:self didFindMapping:attributeMapping forKeyPath:attributeMapping.sourceKeyPath]; } RKLogTrace(@"Mapping attribute value keyPath '%@' to '%@'", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath); - + // Inspect the property type to handle any value transformations Class type = [self.objectMapping classForProperty:attributeMapping.destinationKeyPath]; if (type && NO == [[value class] isSubclassOfClass:type]) { value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type]; } - + // Ensure that the value is different - if ([self shouldSetValue:value atKeyPath:attributeMapping.destinationKeyPath]) { + if ([self shouldSetValue:&value atKeyPath:attributeMapping.destinationKeyPath]) { RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); - - [self.destinationObject setValue:value forKey:attributeMapping.destinationKeyPath]; + + [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]; + } } } @@ -312,23 +355,40 @@ - (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; + } + id value = nil; - if ([attributeMapping.sourceKeyPath isEqualToString:@""]) { - value = self.sourceObject; - } else { - value = [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; + @try { + if ([attributeMapping.sourceKeyPath isEqualToString:@""]) { + value = self.sourceObject; + } else { + value = [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; + } } + @catch (NSException *exception) { + if ([[exception name] isEqualToString:NSUndefinedKeyException] && self.objectMapping.ignoreUnknownKeyPaths) { + RKLogWarning(@"Encountered an undefined attribute mapping for keyPath '%@' that generated NSUndefinedKeyException exception. Skipping due to objectMapping.ignoreUnknownKeyPaths = YES", + attributeMapping.sourceKeyPath); + continue; + } + + @throw; + } + if (value) { appliedMappings = YES; [self applyAttributeMapping:attributeMapping withValue:value]; @@ -337,26 +397,33 @@ - (BOOL)applyAttributeMappings { [self.delegate objectMappingOperation:self didNotFindMappingForKeyPath:attributeMapping.sourceKeyPath]; } RKLogTrace(@"Did not find mappable attribute value keyPath '%@'", attributeMapping.sourceKeyPath); - + // Optionally set the default value for missing values if ([self.objectMapping shouldSetDefaultValueForMissingAttributes]) { - [self.destinationObject setValue:[self.objectMapping defaultValueForMissingAttribute:attributeMapping.destinationKeyPath] - forKey:attributeMapping.destinationKeyPath]; + [self.destinationObject setValue:[self.objectMapping defaultValueForMissingAttribute:attributeMapping.destinationKeyPath] + forKeyPath:attributeMapping.destinationKeyPath]; RKLogTrace(@"Setting nil for missing attribute value at keyPath '%@'", attributeMapping.sourceKeyPath); } } - + // Fail out if an error has occurred if (_validationError) { return NO; } } - + return appliedMappings; } +- (BOOL)isTypeACollection:(Class)type { + Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); + return (type && ([type isSubclassOfClass:[NSSet class]] || + [type isSubclassOfClass:[NSArray class]] || + (orderedSetClass && [type isSubclassOfClass:orderedSetClass]))); +} + - (BOOL)isValueACollection:(id)value { - return ([value isKindOfClass:[NSSet class]] || [value isKindOfClass:[NSArray class]]); + return [self isTypeACollection:[value class]]; } - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRealtionshipMapping:(RKObjectRelationshipMapping*)relationshipMapping { @@ -364,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; @@ -372,30 +439,43 @@ - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRealtionship if (NO == [subOperation performMapping:&error]) { RKLogWarning(@"WARNING: Failed mapping nested object: %@", [error localizedDescription]); } - + return YES; } - (BOOL)applyRelationshipMappings { BOOL appliedMappings = NO; id destinationObject = nil; - + for (RKObjectRelationshipMapping* relationshipMapping in [self relationshipMappings]) { - id value = [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath]; - + id value = nil; + @try { + value = [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath]; + } + @catch (NSException *exception) { + if ([[exception name] isEqualToString:NSUndefinedKeyException] && self.objectMapping.ignoreUnknownKeyPaths) { + RKLogWarning(@"Encountered an undefined relationship mapping for keyPath '%@' that generated NSUndefinedKeyException exception. Skipping due to objectMapping.ignoreUnknownKeyPaths = YES", + relationshipMapping.sourceKeyPath); + continue; + } + + @throw; + } + 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 - if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:nil atKeyPath:relationshipMapping.destinationKeyPath]) { + id nilReference = nil; + if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:&nilReference atKeyPath:relationshipMapping.destinationKeyPath]) { RKLogTrace(@"Setting nil for missing relationship value at keyPath '%@'", relationshipMapping.sourceKeyPath); - [self.destinationObject setValue:nil forKey:relationshipMapping.destinationKeyPath]; + [self.destinationObject setValue:nil forKeyPath:relationshipMapping.destinationKeyPath]; } - + 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]]) { @@ -410,27 +490,29 @@ - (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]; - BOOL mappingToCollection = (relationshipType && - ([relationshipType isSubclassOfClass:[NSSet class]] || [relationshipType isSubclassOfClass:[NSArray class]])); + BOOL mappingToCollection = [self isTypeACollection:relationshipType]; if (mappingToCollection && ![self isValueACollection:value]) { + Class orderedSetClass = NSClassFromString(@"NSOrderedSet"); RKLogDebug(@"Asked to map a single object into a collection relationship. Transforming to an instance of: %@", NSStringFromClass(relationshipType)); if ([relationshipType isSubclassOfClass:[NSArray class]]) { value = [relationshipType arrayWithObject:value]; } else if ([relationshipType isSubclassOfClass:[NSSet class]]) { value = [relationshipType setWithObject:value]; + } else if (orderedSetClass && [relationshipType isSubclassOfClass:orderedSetClass]) { + value = [relationshipType orderedSetWithObject:value]; } else { 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; - + destinationObject = [NSMutableArray arrayWithCapacity:[value count]]; id collectionSanityCheckObject = nil; if ([value respondsToSelector:@selector(anyObject)]) collectionSanityCheckObject = [value anyObject]; @@ -439,8 +521,8 @@ - (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) { - id mapping = relationshipMapping.mapping; + for (id nestedObject in value) { + RKObjectMappingDefinition * mapping = relationshipMapping.mapping; RKObjectMapping* objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:nestedObject]; @@ -458,7 +540,7 @@ - (BOOL)applyRelationshipMappings { [destinationObject addObject:mappedObject]; } } - + // Transform from NSSet <-> NSArray if necessary Class type = [self.objectMapping classForProperty:relationshipMapping.destinationKeyPath]; if (type && NO == [[destinationObject class] isSubclassOfClass:type]) { @@ -466,8 +548,9 @@ - (BOOL)applyRelationshipMappings { } // If the relationship has changed, set it - if ([self shouldSetValue:destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { + if ([self shouldSetValue:&destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { Class managedObjectClass = NSClassFromString(@"NSManagedObject"); + Class nsOrderedSetClass = NSClassFromString(@"NSOrderedSet"); if (managedObjectClass && [self.destinationObject isKindOfClass:managedObjectClass]) { RKLogTrace(@"Found a managedObject collection. About to apply value via mutable[Set|Array]ValueForKey"); if ([destinationObject isKindOfClass:[NSSet class]]) { @@ -478,17 +561,24 @@ - (BOOL)applyRelationshipMappings { RKLogTrace(@"Mapped NSArray relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); NSMutableArray* destinationArray = [self.destinationObject mutableArrayValueForKey:relationshipMapping.destinationKeyPath]; [destinationArray setArray:destinationObject]; + } else if (nsOrderedSetClass && [destinationObject isKindOfClass:nsOrderedSetClass]) { + RKLogTrace(@"Mapped NSOrderedSet relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); + [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; } } else { RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); - [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; + [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); - - id mapping = relationshipMapping.mapping; + 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]]) { objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:value]; @@ -500,20 +590,30 @@ - (BOOL)applyRelationshipMappings { if ([self mapNestedObject:value toObject:destinationObject withRealtionshipMapping:relationshipMapping]) { appliedMappings = YES; } + + // If the relationship has changed, set it + if ([self shouldSetValue:&destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { + 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]; + } + } } - - // If the relationship has changed, set it - if ([self shouldSetValue:destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) { - RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject); - [self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath]; + + // 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; } } - + return appliedMappings; } @@ -535,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]; @@ -543,20 +643,20 @@ - (BOOL)performMapping:(NSError**)error { RKLogDebug(@"Finished mapping operation successfully..."); return YES; } - + if (_validationError) { // We failed out due to validation if (error) *error = _validationError; if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didFailWithError:)]) { [self.delegate objectMappingOperation:self didFailWithError:_validationError]; } - + RKLogError(@"Failed mapping operation: %@", [_validationError localizedDescription]); } else { // We did not find anything to do RKLogDebug(@"Mapping operation did not find any mappable content"); - } - + } + return NO; } diff --git a/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h b/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h new file mode 100644 index 0000000000..1a42c74d4a --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingProvider+Contexts.h @@ -0,0 +1,50 @@ +// +// RKObjectMappingProvider.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKObjectMappingProvider.h" + +// Contexts provide primitives for managing collections of object mappings namespaced +// within a single mapping provider. This enables easy reuse and extension via categories. +@interface RKObjectMappingProvider (Contexts) + +- (void)initializeContext:(RKObjectMappingProviderContext)context withValue:(id)value; +- (id)valueForContext:(RKObjectMappingProviderContext)context; +- (void)setValue:(id)value forContext:(RKObjectMappingProviderContext)context; + +- (RKObjectMappingDefinition *)mappingForContext:(RKObjectMappingProviderContext)context; +/** + Stores a single object mapping for a given context. Useful when a component needs to enable + configuration via one (and only one) object mapping. + */ +- (void)setMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context; +- (NSArray *)mappingsForContext:(RKObjectMappingProviderContext)context; +- (void)addMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context; +- (void)removeMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context; +- (RKObjectMappingDefinition *)mappingForKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context; +- (void)setMapping:(RKObjectMappingDefinition *)mapping forKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context; +- (void)removeMappingForKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context; + +- (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)pattern atIndex:(NSUInteger)index context:(RKObjectMappingProviderContext)context; +- (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)pattern context:(RKObjectMappingProviderContext)context; +- (RKObjectMappingDefinition *)mappingForPatternMatchingString:(NSString *)string context:(RKObjectMappingProviderContext)context; +- (void)setEntry:(RKObjectMappingProviderContextEntry *)entry forPattern:(NSString *)pattern context:(RKObjectMappingProviderContext)context; +- (RKObjectMappingProviderContextEntry *)entryForPatternMatchingString:(NSString *)string context:(RKObjectMappingProviderContext)context; + +@end diff --git a/Code/ObjectMapping/RKObjectMappingProvider.h b/Code/ObjectMapping/RKObjectMappingProvider.h index 67224fe195..7a6cb197ba 100644 --- a/Code/ObjectMapping/RKObjectMappingProvider.h +++ b/Code/ObjectMapping/RKObjectMappingProvider.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 5/6/11. -// Copyright 2011 RestKit -// +// 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,68 +20,84 @@ #import "RKObjectMapping.h" #import "RKDynamicObjectMapping.h" +#import "RKObjectMappingProviderContextEntry.h" + +// Internal framework contexts +// @see RKObjectMappingProvider+Contexts.h +typedef enum { + RKObjectMappingProviderContextObjectsByKeyPath = 1000, + RKObjectMappingProviderContextObjectsByType, + RKObjectMappingProviderContextObjectsByResourcePathPattern, + RKObjectMappingProviderContextSerialization, + RKObjectMappingProviderContextErrors, + RKObjectMappingProviderContextPagination +} RKObjectMappingProviderContext; /** 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] that they target. */ @interface RKObjectMappingProvider : NSObject { - NSMutableArray *_objectMappings; - NSMutableDictionary *_mappingsByKeyPath; - NSMutableDictionary *_serializationMappings; + NSMutableDictionary *mappingContexts; } /** - Returns a new autoreleased object mapping provider - + Creates and returns an autoreleased RKObjectMappingProvider instance. + @return A new autoreleased object mapping provider instance. */ -+ (RKObjectMappingProvider *)objectMappingProvider; ++ (id)mappingProvider; + +/** + Instantiate and return a new auto-released object mapping provider after + yielding it to the specified block for configuration + */ ++ (id)mappingProviderUsingBlock:(void (^)(RKObjectMappingProvider *))block; /** 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 @see RKObjectMappingOperation */ -- (void)setObjectMapping:(id)objectOrDynamicMapping forKeyPath:(NSString *)keyPath; +- (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. */ -- (id)objectMappingForKeyPath:(NSString *)keyPath; +- (RKObjectMappingDefinition *)objectMappingForKeyPath:(NSString *)keyPath; /** 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; @@ -89,7 +105,7 @@ /** 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 @@ -98,35 +114,35 @@ /** 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 */ @@ -136,9 +152,9 @@ 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: @@ -148,7 +164,7 @@ /** 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. */ @@ -156,16 +172,16 @@ /** 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 */ @@ -174,11 +190,11 @@ /** 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] @@ -187,22 +203,87 @@ /** Returns the serialization mapping for a specific object class - which has been previously registered. The - + 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: */ - (RKObjectMapping *)serializationMappingForClass:(Class)objectClass; +/** + 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 + 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 + to determine if objectMapping is the appropriate mapping. + @see RKPathMatcher + @see RKURL + @see RKObjectLoader + */ +- (void)setObjectMapping:(RKObjectMappingDefinition *)objectMapping forResourcePathPattern:(NSString *)resourcePathPattern; + +/** + 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 + or nil if no match was found. + */ +- (RKObjectMappingDefinition *)objectMappingForResourcePath:(NSString *)resourcePath; + + +- (void)setEntry:(RKObjectMappingProviderContextEntry *)entry forResourcePathPattern:(NSString *)resourcePath; +- (RKObjectMappingProviderContextEntry *)entryForResourcePath:(NSString *)resourcePath; + +/** + 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 + */ +@property (nonatomic, retain) RKObjectMapping *errorMapping; + +/** + 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; + @end // Method signatures being phased out @interface RKObjectMappingProvider (CompatibilityAliases) -+ (RKObjectMappingProvider *)mappingProvider; ++ (RKObjectMappingProvider *)objectMappingProvider; - (void)registerMapping:(RKObjectMapping *)objectMapping withRootKeyPath:(NSString *)keyPath; -- (void)setMapping:(id)objectOrDynamicMapping forKeyPath:(NSString *)keyPath; -- (id)mappingForKeyPath:(NSString *)keyPath; +- (void)setMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping forKeyPath:(NSString *)keyPath; +- (RKObjectMappingDefinition *)mappingForKeyPath:(NSString *)keyPath; - (NSDictionary *)mappingsByKeyPath; - (void)removeMappingForKeyPath:(NSString *)keyPath; @end diff --git a/Code/ObjectMapping/RKObjectMappingProvider.m b/Code/ObjectMapping/RKObjectMappingProvider.m index cd15e51bc3..3bf5d4ecad 100644 --- a/Code/ObjectMapping/RKObjectMappingProvider.m +++ b/Code/ObjectMapping/RKObjectMappingProvider.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 5/6/11. -// Copyright 2011 RestKit -// +// 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,52 +19,63 @@ // #import "RKObjectMappingProvider.h" +#import "RKObjectMappingProvider+Contexts.h" +#import "RKOrderedDictionary.h" +#import "RKPathMatcher.h" +#import "RKObjectMappingProviderContextEntry.h" @implementation RKObjectMappingProvider -+ (RKObjectMappingProvider *)objectMappingProvider { ++ (RKObjectMappingProvider *)mappingProvider { return [[self new] autorelease]; } ++ (RKObjectMappingProvider *)mappingProviderUsingBlock:(void (^)(RKObjectMappingProvider *mappingProvider))block { + RKObjectMappingProvider* mappingProvider = [self mappingProvider]; + block(mappingProvider); + return mappingProvider; +} + - (id)init { self = [super init]; if (self) { - _objectMappings = [NSMutableArray new]; - _mappingsByKeyPath = [NSMutableDictionary new]; - _serializationMappings = [NSMutableDictionary new]; + mappingContexts = [NSMutableDictionary new]; + [self initializeContext:RKObjectMappingProviderContextObjectsByKeyPath withValue:[NSMutableDictionary dictionary]]; + [self initializeContext:RKObjectMappingProviderContextObjectsByType withValue:[NSMutableArray array]]; + [self initializeContext:RKObjectMappingProviderContextObjectsByResourcePathPattern withValue:[RKOrderedDictionary dictionary]]; + [self initializeContext:RKObjectMappingProviderContextSerialization withValue:[NSMutableDictionary dictionary]]; + [self initializeContext:RKObjectMappingProviderContextErrors withValue:[NSNull null]]; } return self; } - (void)dealloc { - [_objectMappings release]; - [_mappingsByKeyPath release]; - [_serializationMappings release]; + [mappingContexts release]; [super dealloc]; } -- (void)setObjectMapping:(id)objectOrDynamicMapping forKeyPath:(NSString *)keyPath { - [_mappingsByKeyPath setValue:objectOrDynamicMapping forKey:keyPath]; +- (void)setObjectMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping forKeyPath:(NSString *)keyPath { + [self setMapping:objectOrDynamicMapping forKeyPath:keyPath context:RKObjectMappingProviderContextObjectsByKeyPath]; } - (void)removeObjectMappingForKeyPath:(NSString *)keyPath { - [_mappingsByKeyPath removeObjectForKey:keyPath]; + [self removeMappingForKeyPath:keyPath context:RKObjectMappingProviderContextObjectsByKeyPath]; } -- (id)objectMappingForKeyPath:(NSString *)keyPath { - return [_mappingsByKeyPath objectForKey:keyPath]; +- (RKObjectMappingDefinition *)objectMappingForKeyPath:(NSString *)keyPath { + return [self mappingForKeyPath:keyPath context:RKObjectMappingProviderContextObjectsByKeyPath]; } - (void)setSerializationMapping:(RKObjectMapping *)mapping forClass:(Class)objectClass { - [_serializationMappings setValue:mapping forKey:NSStringFromClass(objectClass)]; + [self setMapping:mapping forKeyPath:NSStringFromClass(objectClass) context:RKObjectMappingProviderContextSerialization]; } -- (RKObjectMapping*)serializationMappingForClass:(Class)objectClass { - return (RKObjectMapping *)[_serializationMappings objectForKey:NSStringFromClass(objectClass)]; +- (RKObjectMapping *)serializationMappingForClass:(Class)objectClass { + return (RKObjectMapping *) [self mappingForKeyPath:NSStringFromClass(objectClass) context:RKObjectMappingProviderContextSerialization]; } - (NSDictionary*)objectMappingsByKeyPath { - return _mappingsByKeyPath; + return [NSDictionary dictionaryWithDictionary:(NSDictionary *) [self valueForContext:RKObjectMappingProviderContextObjectsByKeyPath]]; } - (void)registerObjectMapping:(RKObjectMapping *)objectMapping withRootKeyPath:(NSString *)keyPath { @@ -77,13 +88,15 @@ - (void)registerObjectMapping:(RKObjectMapping *)objectMapping withRootKeyPath:( } - (void)addObjectMapping:(RKObjectMapping *)objectMapping { - [_objectMappings addObject:objectMapping]; + [self addMapping:objectMapping context:RKObjectMappingProviderContextObjectsByType]; } - (NSArray *)objectMappingsForClass:(Class)theClass { NSMutableArray *mappings = [NSMutableArray array]; - NSArray *mappingsToSearch = [[NSArray arrayWithArray:_objectMappings] arrayByAddingObjectsFromArray:[_mappingsByKeyPath allValues]]; - for (NSObject *candidateMapping in mappingsToSearch) { + NSArray *mappingByType = [self valueForContext:RKObjectMappingProviderContextObjectsByType]; + NSArray *mappingByKeyPath = [[self valueForContext:RKObjectMappingProviderContextObjectsByKeyPath] allValues]; + NSArray *mappingsToSearch = [[NSArray arrayWithArray:mappingByType] arrayByAddingObjectsFromArray:mappingByKeyPath]; + for (RKObjectMappingDefinition *candidateMapping in mappingsToSearch) { if ( ![candidateMapping respondsToSelector:@selector(objectClass)] || [mappings containsObject:candidateMapping]) continue; Class mappedClass = [candidateMapping performSelector:@selector(objectClass)]; @@ -99,10 +112,194 @@ - (RKObjectMapping *)objectMappingForClass:(Class)theClass { return ([objectMappings count] > 0) ? [objectMappings objectAtIndex:0] : nil; } +#pragma mark - Error Mappings + +- (RKObjectMapping *)errorMapping { + return (RKObjectMapping *) [self mappingForContext:RKObjectMappingProviderContextErrors]; +} + +- (void)setErrorMapping:(RKObjectMapping *)errorMapping { + if (errorMapping) { + [self setMapping:errorMapping context:RKObjectMappingProviderContextErrors]; + } +} + +#pragma mark - Pagination Mapping + +- (RKObjectMapping *)paginationMapping { + return (RKObjectMapping *) [self mappingForContext:RKObjectMappingProviderContextPagination]; +} + +- (void)setPaginationMapping:(RKObjectMapping *)paginationMapping { + [self setMapping:paginationMapping context:RKObjectMappingProviderContextPagination]; +} + +- (void)setObjectMapping:(RKObjectMappingDefinition *)objectMapping forResourcePathPattern:(NSString *)resourcePath { + [self setMapping:objectMapping forPattern:resourcePath context:RKObjectMappingProviderContextObjectsByResourcePathPattern]; +} + +- (RKObjectMappingDefinition *)objectMappingForResourcePath:(NSString *)resourcePath { + return [self mappingForPatternMatchingString:resourcePath context:RKObjectMappingProviderContextObjectsByResourcePathPattern]; +} + +- (void)setEntry:(RKObjectMappingProviderContextEntry *)entry forResourcePathPattern:(NSString *)resourcePath { + [self setEntry:entry forPattern:resourcePath context:RKObjectMappingProviderContextObjectsByResourcePathPattern]; +} + +- (RKObjectMappingProviderContextEntry *)entryForResourcePath:(NSString *)resourcePath { + return [self entryForPatternMatchingString:resourcePath context:RKObjectMappingProviderContextObjectsByResourcePathPattern]; +} + +#pragma mark - Mapping Context Primitives + +- (void)initializeContext:(RKObjectMappingProviderContext)context withValue:(id)value { + NSAssert([self valueForContext:context] == nil, @"Attempt to reinitialized an existing mapping provider context."); + [self setValue:value forContext:context]; +} + +- (id)valueForContext:(RKObjectMappingProviderContext)context { + NSNumber *contextNumber = [NSNumber numberWithInteger:context]; + return [mappingContexts objectForKey:contextNumber]; +} + +- (void)setValue:(id)value forContext:(RKObjectMappingProviderContext)context { + NSNumber *contextNumber = [NSNumber numberWithInteger:context]; + [mappingContexts setObject:value forKey:contextNumber]; +} + +- (void)assertStorageForContext:(RKObjectMappingProviderContext)context isKindOfClass:(Class)theClass { + id contextValue = [self valueForContext:context]; + NSAssert([contextValue isKindOfClass:theClass], @"Storage type mismatch for context %d: expected a %@, got %@.", context, theClass, [contextValue class]); +} + +- (void)setMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context { + NSNumber *contextNumber = [NSNumber numberWithInteger:context]; + [mappingContexts setObject:mapping forKey:contextNumber]; +} + +- (RKObjectMappingDefinition *)mappingForContext:(RKObjectMappingProviderContext)context { + id contextValue = [self valueForContext:context]; + if ([contextValue isEqual:[NSNull null]]) return nil; + Class class = [RKObjectMappingDefinition class]; + NSAssert([contextValue isKindOfClass:class], @"Storage type mismatch for context %d: expected a %@, got %@.", context, class, [contextValue class]); + return contextValue; +} + +- (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]; +} + +- (void)addMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context { + NSMutableArray *contextValue = [self valueForContext:context]; + if (contextValue == nil) { + contextValue = [NSMutableArray arrayWithCapacity:1]; + [self setValue:contextValue forContext:context]; + } + [self assertStorageForContext:context isKindOfClass:[NSArray class]]; + [contextValue addObject:mapping]; +} + +- (void)removeMapping:(RKObjectMappingDefinition *)mapping context:(RKObjectMappingProviderContext)context { + NSMutableArray *contextValue = [self valueForContext:context]; + NSAssert(contextValue, @"Attempted to remove mapping from undefined context: %d", context); + [self assertStorageForContext:context isKindOfClass:[NSArray class]]; + NSAssert([contextValue containsObject:mapping], @"Attempted to remove mapping from collection that does not include it for context: %d", context); + [contextValue removeObject:mapping]; +} + +- (RKObjectMappingDefinition *)mappingForKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context { + NSMutableDictionary *contextValue = [self valueForContext:context]; + NSAssert(contextValue, @"Attempted to retrieve mapping from undefined context: %d", context); + [self assertStorageForContext:context isKindOfClass:[NSDictionary class]]; + return [contextValue valueForKey:keyPath]; +} + +- (void)setMapping:(RKObjectMappingDefinition *)mapping forKeyPath:(NSString *)keyPath context:(RKObjectMappingProviderContext)context { + NSMutableDictionary *contextValue = [self valueForContext:context]; + if (contextValue == nil) { + contextValue = [NSMutableDictionary dictionary]; + [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]]; + [contextValue removeObjectForKey:keyPath]; +} + +- (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)pattern atIndex:(NSUInteger)index context:(RKObjectMappingProviderContext)context { + RKOrderedDictionary *contextValue = [self valueForContext:context]; + if (contextValue == nil) { + contextValue = [RKOrderedDictionary dictionary]; + [self setValue:contextValue forContext:context]; + } + [self assertStorageForContext:context isKindOfClass:[RKOrderedDictionary class]]; + [contextValue insertObject:[RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping] + forKey:pattern + atIndex:index]; +} + +- (void)setMapping:(RKObjectMappingDefinition *)mapping forPattern:(NSString *)pattern context:(RKObjectMappingProviderContext)context { + RKOrderedDictionary *contextValue = [self valueForContext:context]; + if (contextValue == nil) { + contextValue = [RKOrderedDictionary dictionary]; + [self setValue:contextValue forContext:context]; + } + [self assertStorageForContext:context isKindOfClass:[RKOrderedDictionary class]]; + [contextValue setObject:[RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping] + forKey:pattern]; +} + +- (RKObjectMappingDefinition *)mappingForPatternMatchingString:(NSString *)string context:(RKObjectMappingProviderContext)context { + NSAssert(string, @"Cannot look up mapping matching nil pattern string."); + RKOrderedDictionary *contextValue = [self valueForContext:context]; + NSAssert(contextValue, @"Attempted to retrieve mapping from undefined context: %d", context); + for (NSString *pattern in contextValue) { + RKPathMatcher *pathMatcher = [RKPathMatcher matcherWithPattern:pattern]; + if ([pathMatcher matchesPath:string tokenizeQueryStrings:NO parsedArguments:nil]) { + RKObjectMappingProviderContextEntry *entry = [contextValue objectForKey:pattern]; + return entry.mapping; + } + } + + return nil; +} + +- (void)setEntry:(RKObjectMappingProviderContextEntry *)entry forPattern:(NSString *)pattern context:(RKObjectMappingProviderContext)context { + RKOrderedDictionary *contextValue = [self valueForContext:context]; + if (contextValue == nil) { + contextValue = [RKOrderedDictionary dictionary]; + [self setValue:contextValue forContext:context]; + } + [self assertStorageForContext:context isKindOfClass:[RKOrderedDictionary class]]; + [contextValue setObject:entry + forKey:pattern]; +} + +- (RKObjectMappingProviderContextEntry *)entryForPatternMatchingString:(NSString *)string context:(RKObjectMappingProviderContext)context { + RKOrderedDictionary *contextValue = [self valueForContext:context]; + NSAssert(contextValue, @"Attempted to retrieve mapping from undefined context: %d", context); + for (NSString *pattern in contextValue) { + RKPathMatcher *pathMatcher = [RKPathMatcher matcherWithPattern:pattern]; + if ([pathMatcher matchesPath:string tokenizeQueryStrings:NO parsedArguments:nil]) { + return [contextValue objectForKey:pattern]; + } + } + + return nil; +} + #pragma mark - Aliases -+ (RKObjectMappingProvider *)mappingProvider { - return [self objectMappingProvider]; ++ (RKObjectMappingProvider *)objectMappingProvider { + return [self mappingProvider]; } - (RKObjectMapping *)mappingForKeyPath:(NSString *)keyPath { @@ -125,4 +322,9 @@ - (void)removeMappingForKeyPath:(NSString *)keyPath { [self removeObjectMappingForKeyPath:keyPath]; } +// Deprecated ++ (id)mappingProviderWithBlock:(void (^)(RKObjectMappingProvider*))block { + return [self mappingProviderUsingBlock:block]; +} + @end diff --git a/Code/ObjectMapping/RKObjectMappingProviderContextEntry.h b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.h new file mode 100644 index 0000000000..746dfacc58 --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.h @@ -0,0 +1,19 @@ +// +// RKObjectMappingProviderContextEntry.h +// RestKit +// +// Created by Jeff Arena on 1/26/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingDefinition.h" + +@interface RKObjectMappingProviderContextEntry : NSObject + ++ (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappingDefinition *)mapping; ++ (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappingDefinition *)mapping userData:(id)userData; + +@property (nonatomic, retain) RKObjectMappingDefinition *mapping; +@property (nonatomic, retain) id userData; + +@end diff --git a/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m new file mode 100644 index 0000000000..da328f183d --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappingProviderContextEntry.m @@ -0,0 +1,60 @@ +// +// RKObjectMappingProviderContextEntry.m +// RestKit +// +// Created by Jeff Arena on 1/26/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectMappingProviderContextEntry.h" + +@implementation RKObjectMappingProviderContextEntry + +@synthesize mapping = _mapping; +@synthesize userData = _userData; + +- (id)init { + self = [super init]; + if (self) { + _mapping = nil; + _userData = nil; + } + return self; +} + +- (void)dealloc { + [_mapping release]; + _mapping = nil; + [_userData release]; + _userData = nil; + [super dealloc]; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[RKObjectMappingProviderContextEntry class]]) { + RKObjectMappingProviderContextEntry *entry = (RKObjectMappingProviderContextEntry *)object; + return ([self.mapping isEqual:entry.mapping] && (self.userData == entry.userData)); + } + return NO; +} + +- (NSUInteger)hash { + int prime = 31; + int result = 1; + result = prime * [self.userData hash]? [self.mapping hash] : [self.userData hash]; + return result; +} + ++ (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappingDefinition *)mapping { + RKObjectMappingProviderContextEntry *contextEntry = [[[RKObjectMappingProviderContextEntry alloc] init] autorelease]; + contextEntry.mapping = mapping; + return contextEntry; +} + ++ (RKObjectMappingProviderContextEntry *)contextEntryWithMapping:(RKObjectMappingDefinition *)mapping userData:(id)userData { + RKObjectMappingProviderContextEntry *contextEntry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; + contextEntry.userData = userData; + return contextEntry; +} + +@end diff --git a/Code/ObjectMapping/RKObjectMappingResult.h b/Code/ObjectMapping/RKObjectMappingResult.h index 282d3b5a6f..07acbbf698 100644 --- a/Code/ObjectMapping/RKObjectMappingResult.h +++ b/Code/ObjectMapping/RKObjectMappingResult.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/7/11. -// Copyright 2011 Two Toasters -// +// 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 d8269db201..02061f3919 100644 --- a/Code/ObjectMapping/RKObjectMappingResult.m +++ b/Code/ObjectMapping/RKObjectMappingResult.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/7/11. -// Copyright 2011 Two Toasters -// +// 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,8 +83,8 @@ - (NSError*)asError { } NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:collection, RKObjectMapperErrorObjectsKey, description, NSLocalizedDescriptionKey, nil]; - - NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKObjectMapperErrorFromMappingResult userInfo:userInfo]; + + NSError* error = [NSError errorWithDomain:RKErrorDomain code:RKObjectMapperErrorFromMappingResult userInfo:userInfo]; return error; } diff --git a/Code/ObjectMapping/RKObjectPaginator.h b/Code/ObjectMapping/RKObjectPaginator.h new file mode 100644 index 0000000000..20c5564eec --- /dev/null +++ b/Code/ObjectMapping/RKObjectPaginator.h @@ -0,0 +1,281 @@ +// +// RKObjectPaginator.h +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKObjectMappingProvider.h" +#import "RKManagedObjectStore.h" +#import "RKObjectLoader.h" +#import "RKConfigurationDelegate.h" + +@protocol RKObjectPaginatorDelegate; + +typedef void(^RKObjectPaginatorDidLoadObjectsForPageBlock)(NSArray *objects, NSUInteger page); +typedef void(^RKObjectPaginatorDidFailWithErrorBlock)(NSError *error, RKObjectLoader *loader); + +/** + Instances of RKObjectPaginator retrieve paginated collections of mappable data + 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. + */ +@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 + resource collection. + @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. + */ ++ (id)paginatorWithPatternURL:(RKURL *)patternURL mappingProvider:(RKObjectMappingProvider *)mappingProvider; + +/** + 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 + resource collection. + @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. + */ +- (id)initWithPatternURL:(RKURL *)patternURL mappingProvider:(RKObjectMappingProvider *)mappingProvider; + +/** + 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 + */ +@property (nonatomic, readonly) RKURL *URL; + +/** + The object that acts as the delegate of the receiving paginator. + */ +@property (nonatomic, assign) id delegate; + +/** + 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 + and utilized by the paginator. + + **Default**: nil + @see RKClient + @see RKObjectManager + */ +@property (nonatomic, assign) id configurationDelegate; + +/** @name Object Mapping Configuration */ + +/** + The mapping provider to use when performing object mapping on the data + loaded from the remote system. The provider will be assigned to the RKObjectLoader + instance built to retrieve the paginated resource collection. + */ +@property (nonatomic, retain) RKObjectMappingProvider *mappingProvider; + +/** + An object store for accessing Core Data. Required if the objects being paginated + are stored into Core Data. + */ +@property (nonatomic, retain) RKManagedObjectStore *objectStore; + +/** @name Pagination Metadata */ + +/** + The number of objects to load per page + */ +@property (nonatomic, assign) NSUInteger perPage; + +/** + 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. + */ +@property (nonatomic, readonly) NSUInteger currentPage; + +/** + 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. + */ +@property (nonatomic, readonly) NSUInteger pageCount; + +/** + 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. + */ +@property (nonatomic, readonly) NSUInteger objectCount; + +/** + 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; + +/** + 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. + */ +- (BOOL)hasNextPage; + +/** + 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. + */ +- (BOOL)hasPreviousPage; + +/** @name Paginator Actions */ + +/** + Loads the next page of data by incrementing the current page, constructing an object + loader to fetch the data, and object mapping the results. + */ +- (void)loadNextPage; + +/** + Loads the previous page of data by decrementing the current page, constructing an object + loader to fetch the data, and object mapping the results. + */ +- (void)loadPreviousPage; + +/** + 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; + +@end + +/** + The RKObjectPaginatorDelegate formal protocol defines + RKObjectPaginator delegate methods that can be implemented by + objects to receive informational callbacks about paginated loading + of mapping objects through RestKit. + */ +@protocol RKObjectPaginatorDelegate + +/** + 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. + */ +- (void)paginator:(RKObjectPaginator *)paginator didLoadObjects:(NSArray *)objects forPage:(NSUInteger)page; + +/** + 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. + */ +- (void)paginator:(RKObjectPaginator *)paginator didFailWithError:(NSError *)error objectLoader:(RKObjectLoader *)loader; + +@optional + +/** + 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. + */ +- (void)paginator:(RKObjectPaginator *)paginator willLoadPage:(NSUInteger)page objectLoader:(RKObjectLoader *)loader; + +/** + 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; + +@end diff --git a/Code/ObjectMapping/RKObjectPaginator.m b/Code/ObjectMapping/RKObjectPaginator.m new file mode 100644 index 0000000000..71e10b196f --- /dev/null +++ b/Code/ObjectMapping/RKObjectPaginator.m @@ -0,0 +1,220 @@ +// +// RKObjectPaginator.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKObjectPaginator.h" +#import "RKManagedObjectLoader.h" +#import "RKObjectMappingOperation.h" +#import "SOCKit.h" +#import "RKLog.h" + +static NSUInteger RKObjectPaginatorDefaultPerPage = 25; + +// Private interface +@interface RKObjectPaginator () +@property (nonatomic, retain) RKObjectLoader *objectLoader; +@end + +@implementation RKObjectPaginator + +@synthesize patternURL; +@synthesize currentPage; +@synthesize perPage; +@synthesize loaded; +@synthesize pageCount; +@synthesize objectCount; +@synthesize mappingProvider; +@synthesize delegate; +@synthesize objectStore; +@synthesize objectLoader; +@synthesize configurationDelegate; +@synthesize onDidLoadObjectsForPage; +@synthesize onDidFailWithError; + ++ (id)paginatorWithPatternURL:(RKURL *)aPatternURL mappingProvider:(RKObjectMappingProvider *)aMappingProvider { + return [[[self alloc] initWithPatternURL:aPatternURL mappingProvider:aMappingProvider] autorelease]; +} + +- (id)initWithPatternURL:(RKURL *)aPatternURL mappingProvider:(RKObjectMappingProvider *)aMappingProvider { + self = [super init]; + if (self) { + patternURL = [aPatternURL copy]; + mappingProvider = [aMappingProvider retain]; + currentPage = NSUIntegerMax; + pageCount = NSUIntegerMax; + objectCount = NSUIntegerMax; + perPage = RKObjectPaginatorDefaultPerPage; + loaded = NO; + } + + return self; +} + +- (void)dealloc { + delegate = nil; + configurationDelegate = nil; + objectLoader.delegate = nil; + [patternURL release]; + patternURL = nil; + [mappingProvider release]; + mappingProvider = nil; + [objectStore release]; + objectStore = nil; + [objectLoader cancel]; + objectLoader.delegate = nil; + [objectLoader release]; + objectLoader = nil; + [onDidLoadObjectsForPage release]; + onDidLoadObjectsForPage = nil; + [onDidFailWithError release]; + onDidFailWithError = nil; + + [super dealloc]; +} + +- (RKObjectMapping *)paginationMapping { + return [mappingProvider paginationMapping]; +} + +- (RKURL *)URL { + return [patternURL URLByInterpolatingResourcePathWithObject:self]; +} + +// Private. Public consumers can rely on isLoaded +- (BOOL)hasCurrentPage { + return currentPage != NSUIntegerMax; +} + +- (BOOL)hasPageCount { + return pageCount != NSUIntegerMax; +} + +- (BOOL)hasObjectCount { + return objectCount != NSUIntegerMax; +} + +- (NSUInteger)currentPage { + // Referenced during initial load, so we don't rely on isLoaded. + NSAssert([self hasCurrentPage], @"Current page has not been initialized."); + return currentPage; +} + +- (NSUInteger)pageCount { + NSAssert([self hasPageCount], @"Page count not available."); + return pageCount; +} + +- (BOOL)hasNextPage { + NSAssert(self.isLoaded, @"Cannot determine hasNextPage: paginator is not loaded."); + NSAssert([self hasPageCount], @"Cannot determine hasNextPage: page count is not known."); + + return self.currentPage < self.pageCount; +} + +- (BOOL)hasPreviousPage { + NSAssert(self.isLoaded, @"Cannot determine hasPreviousPage: paginator is not loaded."); + return self.currentPage > 1; +} + +#pragma mark - RKObjectLoaderDelegate methods + +- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects { + self.objectLoader = nil; + 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]; + } + } +} + +- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error { + RKLogError(@"Paginator error %@", error); + [self.delegate paginator:self didFailWithError:error objectLoader:self.objectLoader]; + if (self.onDidFailWithError) { + self.onDidFailWithError(error, self.objectLoader); + } + self.objectLoader = nil; +} + +- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData { + NSError *error = nil; + RKObjectMappingOperation *mappingOperation = [RKObjectMappingOperation mappingOperationFromObject:*mappableData toObject:self withMapping:[self paginationMapping]]; + BOOL success = [mappingOperation performMapping:&error]; + if (!success) { + pageCount = currentPage = 0; + RKLogError(@"Paginator didn't map info to compute page count. Assuming no pages."); + } 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); + } else { + NSAssert(NO, @"Paginator perPage set is 0."); + RKLogError(@"Paginator perPage set is 0."); + } +} + +#pragma mark - Action methods + +- (void)loadNextPage { + [self loadPage:currentPage + 1]; +} + +- (void)loadPreviousPage { + [self loadPage:currentPage - 1]; +} + +- (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]; +} + +@end diff --git a/Code/ObjectMapping/RKObjectPropertyInspector.h b/Code/ObjectMapping/RKObjectPropertyInspector.h index 44cfca58c5..1008a418e6 100644 --- a/Code/ObjectMapping/RKObjectPropertyInspector.h +++ b/Code/ObjectMapping/RKObjectPropertyInspector.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 3/4/10. -// Copyright 2010 Two Toasters -// +// 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; @@ -38,4 +38,10 @@ */ - (Class)typeForProperty:(NSString*)propertyName ofClass:(Class)objectClass; +/** + Returns the name of a property when provided the name of a property obtained + via the property_getAttributes reflection API + */ ++ (NSString *)propertyTypeFromAttributeString:(NSString *)attributeString; + @end diff --git a/Code/ObjectMapping/RKObjectPropertyInspector.m b/Code/ObjectMapping/RKObjectPropertyInspector.m index d1d3b2880a..30f501f6f8 100644 --- a/Code/ObjectMapping/RKObjectPropertyInspector.m +++ b/Code/ObjectMapping/RKObjectPropertyInspector.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 3/4/10. -// Copyright 2010 Two Toasters -// +// 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*)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; } - (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 = [[self 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 e5c23741cc..e2b4c26a1b 100644 --- a/Code/ObjectMapping/RKObjectRelationshipMapping.h +++ b/Code/ObjectMapping/RKObjectRelationshipMapping.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/4/11. -// Copyright 2011 Two Toasters -// +// 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,15 +25,15 @@ @class RKObjectmapping; @interface RKObjectRelationshipMapping : RKObjectAttributeMapping { - id _mapping; + RKObjectMappingDefinition * _mapping; BOOL _reversible; } -@property (nonatomic, retain) id mapping; +@property (nonatomic, retain) RKObjectMappingDefinition * mapping; @property (nonatomic, assign) BOOL reversible; -+ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(id)objectOrDynamicMapping; ++ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping; -+ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(id)objectOrDynamicMapping reversible:(BOOL)reversible; ++ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping reversible:(BOOL)reversible; @end diff --git a/Code/ObjectMapping/RKObjectRelationshipMapping.m b/Code/ObjectMapping/RKObjectRelationshipMapping.m index ddbde06695..555ea7eb8b 100644 --- a/Code/ObjectMapping/RKObjectRelationshipMapping.m +++ b/Code/ObjectMapping/RKObjectRelationshipMapping.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/4/11. -// Copyright 2011 Two Toasters -// +// 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,14 +25,14 @@ @implementation RKObjectRelationshipMapping @synthesize mapping = _mapping; @synthesize reversible = _reversible; -+ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(id)objectOrDynamicMapping reversible:(BOOL)reversible { - RKObjectRelationshipMapping* relationshipMapping = (RKObjectRelationshipMapping*) [self mappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath]; ++ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping reversible:(BOOL)reversible { + RKObjectRelationshipMapping* relationshipMapping = (RKObjectRelationshipMapping*) [self mappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath]; relationshipMapping.reversible = reversible; relationshipMapping.mapping = objectOrDynamicMapping; return relationshipMapping; } -+ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(id)objectOrDynamicMapping { ++ (RKObjectRelationshipMapping*)mappingFromKeyPath:(NSString*)sourceKeyPath toKeyPath:(NSString*)destinationKeyPath withMapping:(RKObjectMappingDefinition *)objectOrDynamicMapping { return [self mappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath withMapping:objectOrDynamicMapping reversible:YES]; } diff --git a/Code/ObjectMapping/RKObjectRouter.h b/Code/ObjectMapping/RKObjectRouter.h index aa0328b4c5..a340266ceb 100644 --- a/Code/ObjectMapping/RKObjectRouter.h +++ b/Code/ObjectMapping/RKObjectRouter.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 10/18/10. -// Copyright 2010 Two Toasters -// +// 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,6 +19,7 @@ // #import "RKRequest.h" +#import "RKRouter.h" // TODO: Cleanup the comments in here @@ -29,44 +30,46 @@ * or DELETE action is invoked. Dynamic routes are available by encoding key paths into * the resourcePath using a single colon delimiter, such as /users/:userID */ -@interface RKObjectRouter : NSObject { - NSMutableDictionary* _routes; +@interface RKObjectRouter : NSObject { + 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 */ -- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath; +- (void)routeClass:(Class)objectClass toResourcePathPattern:(NSString*)resourcePathPattern; /** * Register a mapping from an object class to a resource path for a specific HTTP method. * @see RKPathMatcher */ -- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method; +- (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 */ -- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes; +- (void)routeClass:(Class)objectClass toResourcePathPattern:(NSString*)resourcePathPattern forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes; -/** - * Returns the resource path to send requests for a given object and HTTP method - */ -- (NSString*)resourcePathForObject:(NSObject*)object method:(RKRequestMethod)method; +@end +// Method signatures being phased out +@interface RKObjectRouter (CompatibilityAliases) +- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath; +- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method; +- (void)routeClass:(Class)objectClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes; @end diff --git a/Code/ObjectMapping/RKObjectRouter.m b/Code/ObjectMapping/RKObjectRouter.m index 1acb66762b..3a5dd486f1 100644 --- a/Code/ObjectMapping/RKObjectRouter.m +++ b/Code/ObjectMapping/RKObjectRouter.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 10/18/10. -// Copyright 2010 Two Toasters -// +// 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,7 +19,7 @@ // #import "RKObjectRouter.h" -#import "RKClient.h" +#import "RKPathMatcher.h" #import "NSDictionary+RKRequestSerialization.h" @implementation RKObjectRouter @@ -37,24 +37,25 @@ - (void)dealloc { [super dealloc]; } -- (void)routeClass:(Class)theClass toResourcePath:(NSString*)resourcePath forMethodName:(NSString*)methodName escapeRoutedPath:(BOOL)addEscapes { - NSString* className = NSStringFromClass(theClass); +- (void)routeClass:(Class)theClass toResourcePathPattern:(NSString *)resourcePathPattern forMethodName:(NSString *)methodName escapeRoutedPath:(BOOL)addEscapes { + NSString *className = NSStringFromClass(theClass); if (nil == [_routes objectForKey:theClass]) { - NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; [_routes setObject:dictionary forKey:theClass]; } - NSMutableDictionary* classRoutes = [_routes objectForKey:theClass]; + NSMutableDictionary *classRoutes = [_routes objectForKey:theClass]; if ([classRoutes objectForKey:methodName]) { [NSException raise:nil format:@"A route has already been registered for class '%@' and HTTP method '%@'", className, methodName]; } NSMutableDictionary *routeEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys: - resourcePath, @"resourcePath", [NSNumber numberWithBool:addEscapes], @"addEscapes", nil]; + resourcePathPattern, @"resourcePath", + [NSNumber numberWithBool:addEscapes], @"addEscapes", nil]; [classRoutes setValue:routeEntry forKey:methodName]; } -- (NSString*)HTTPVerbForMethod:(RKRequestMethod)method { +- (NSString *)HTTPVerbForMethod:(RKRequestMethod)method { switch (method) { case RKRequestMethodGET: return @"GET"; @@ -76,25 +77,25 @@ - (NSString*)HTTPVerbForMethod:(RKRequestMethod)method { // Public -- (void)routeClass:(Class)theClass toResourcePath:(NSString*)resourcePath { - [self routeClass:theClass toResourcePath:resourcePath forMethodName:@"ANY" escapeRoutedPath:YES]; +- (void)routeClass:(Class)theClass toResourcePathPattern:(NSString *)resourcePathPattern { + [self routeClass:theClass toResourcePathPattern:resourcePathPattern forMethodName:@"ANY" escapeRoutedPath:YES]; } -- (void)routeClass:(Class)theClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method { +- (void)routeClass:(Class)theClass toResourcePathPattern:(NSString *)resourcePath forMethod:(RKRequestMethod)method { [self routeClass:theClass toResourcePath:resourcePath forMethod:method escapeRoutedPath:YES]; } -- (void)routeClass:(Class)theClass toResourcePath:(NSString*)resourcePath forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes { - NSString* methodName = [self HTTPVerbForMethod:method]; - [self routeClass:theClass toResourcePath:resourcePath forMethodName:methodName escapeRoutedPath:addEscapes]; +- (void)routeClass:(Class)theClass toResourcePathPattern:(NSString *)resourcePath forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes { + NSString *methodName = [self HTTPVerbForMethod:method]; + [self routeClass:theClass toResourcePathPattern:resourcePath forMethodName:methodName escapeRoutedPath:addEscapes]; } #pragma mark RKRouter -- (NSString*)resourcePathForObject:(NSObject*)object method:(RKRequestMethod)method { - NSString* methodName = [self HTTPVerbForMethod:method]; - NSString* className = NSStringFromClass([object class]); - NSDictionary* classRoutes = nil; +- (NSString *)resourcePathForObject:(NSObject *)object method:(RKRequestMethod)method { + NSString *methodName = [self HTTPVerbForMethod:method]; + NSString *className = NSStringFromClass([object class]); + NSDictionary *classRoutes = nil; // Check for exact matches for (Class possibleClass in _routes) { @@ -120,13 +121,31 @@ - (NSString*)resourcePathForObject:(NSObject*)object method:(RKRequestMethod)met if (routeEntry) { BOOL addEscapes = [[routeEntry objectForKey:@"addEscapes"] boolValue]; - NSString *path = RKMakePathWithObjectAddingEscapes([routeEntry objectForKey:@"resourcePath"], object, addEscapes); - return path; + RKPathMatcher *matcher = [RKPathMatcher matcherWithPattern:[routeEntry objectForKey:@"resourcePath"]]; + NSString *interpolatedPath = [matcher pathFromObject:object addingEscapes:addEscapes]; + + 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; } @end + +@implementation RKObjectRouter (CompatibilityAliases) + +- (void)routeClass:(Class)objectClass toResourcePath:(NSString *)resourcePath { + [self routeClass:objectClass toResourcePathPattern:resourcePath]; +} + +- (void)routeClass:(Class)objectClass toResourcePath:(NSString *)resourcePath forMethod:(RKRequestMethod)method { + [self routeClass:objectClass toResourcePathPattern:resourcePath forMethod:method]; +} + +- (void)routeClass:(Class)objectClass toResourcePath:(NSString *)resourcePath forMethod:(RKRequestMethod)method escapeRoutedPath:(BOOL)addEscapes { + [self routeClass:objectClass toResourcePathPattern:resourcePath forMethod:method escapeRoutedPath:addEscapes]; +} + +@end diff --git a/Code/ObjectMapping/RKObjectSerializer.h b/Code/ObjectMapping/RKObjectSerializer.h index f3ca1d4d62..87758e672c 100644 --- a/Code/ObjectMapping/RKObjectSerializer.h +++ b/Code/ObjectMapping/RKObjectSerializer.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/2/11. -// Copyright 2011 Two Toasters -// +// 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 6163935b25..aecdcf890c 100644 --- a/Code/ObjectMapping/RKObjectSerializer.m +++ b/Code/ObjectMapping/RKObjectSerializer.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/2/11. -// Copyright 2011 Two Toasters -// +// 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,14 +83,14 @@ - (id)serializedObjectForMIMEType:(NSString*)MIMEType error:(NSError**)error { if (string == nil) { return nil; } - + return string; } - + return nil; } -- (id)serializationForMIMEType:(NSString*)MIMEType error:(NSError**)error { +- (id)serializationForMIMEType:(NSString *)MIMEType error:(NSError **)error { if ([MIMEType isEqualToString:RKMIMETypeFormURLEncoded]) { // Dictionaries are natively RKRequestSerializable as Form Encoded return [self serializedObject:error]; @@ -101,7 +101,7 @@ - (id)serializedObjectForMIMEType:(NSString*)MIMEType error:(NSError**)error { return [RKRequestSerialization serializationWithData:data MIMEType:MIMEType]; } } - + return nil; } @@ -109,17 +109,21 @@ - (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) { - transformedValue = [self.mapping.preferredDateFormatter stringFromDate:value]; + transformedValue = [self.mapping.preferredDateFormatter stringForObjectValue:value]; } } 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 d0ef74096f..0e0d4c3fdd 100644 --- a/Code/ObjectMapping/RKParserRegistry.h +++ b/Code/ObjectMapping/RKParserRegistry.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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,39 +22,76 @@ #import "RKParser.h" /** - The Parser Registry provides for the registration of RKParser classes - for a particular MIME Type. This enables - */ + RKParserRegistry provides for the registration of RKParser classes + that handle parsing/serializing for content by MIME Type. Registration + is configured via exact string matches (i.e. application/json) or via regular + expression. +*/ @interface RKParserRegistry : NSObject { NSMutableDictionary *_MIMETypeToParserClasses; + 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; /** - Instantiate and return a Parser for 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 An instance of the RKParser conformant class registered to handle the given MIME Type. */ - (id)parserForMIMEType:(NSString *)MIMEType; /** - Return the class registered for handling parser/encoder operations - for a 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. */ - (Class)parserClassForMIMEType:(NSString *)MIMEType; /** - Registers an RKParser conformant class as the handler for the specified MIME Type + 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. */ - (void)setParserClass:(Class)parserClass forMIMEType:(NSString *)MIMEType; +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1070 || __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 + +/** + 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. + */ +- (void)setParserClass:(Class)parserClass forMIMETypeRegularExpression:(NSRegularExpression *)MIMETypeRegex; + +#endif + /** Automatically configure the registry via run-time reflection of the RKParser classes available that ship with RestKit. This happens automatically when the shared registry diff --git a/Code/ObjectMapping/RKParserRegistry.m b/Code/ObjectMapping/RKParserRegistry.m index 1d1f6f0d8e..16ede88d42 100644 --- a/Code/ObjectMapping/RKParserRegistry.m +++ b/Code/ObjectMapping/RKParserRegistry.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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,20 +20,20 @@ #import "RKParserRegistry.h" -RKParserRegistry* gSharedRegistry; +RKParserRegistry *gSharedRegistry; @implementation RKParserRegistry -+ (RKParserRegistry*)sharedRegistry { ++ (RKParserRegistry *)sharedRegistry { if (gSharedRegistry == nil) { gSharedRegistry = [RKParserRegistry new]; [gSharedRegistry autoconfigure]; } - + return gSharedRegistry; } -+ (void)setSharedRegistry:(RKParserRegistry*)registry { ++ (void)setSharedRegistry:(RKParserRegistry *)registry { [registry retain]; [gSharedRegistry release]; gSharedRegistry = registry; @@ -43,48 +43,73 @@ - (id)init { self = [super init]; if (self) { _MIMETypeToParserClasses = [[NSMutableDictionary alloc] init]; + _MIMETypeToParserClassesRegularExpressions = [[NSMutableArray alloc] init]; } - + return self; } - (void)dealloc { [_MIMETypeToParserClasses release]; + [_MIMETypeToParserClassesRegularExpressions release]; [super dealloc]; } -- (Class)parserClassForMIMEType:(NSString*)MIMEType { - return [_MIMETypeToParserClasses objectForKey:MIMEType]; +- (Class)parserClassForMIMEType:(NSString *)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; + } + } + } +#endif + return parserClass; } -- (void)setParserClass:(Class)parserClass forMIMEType:(NSString*)MIMEType { +- (void)setParserClass:(Class)parserClass forMIMEType:(NSString *)MIMEType { [_MIMETypeToParserClasses setObject:parserClass forKey:MIMEType]; } -- (id)parserForMIMEType:(NSString*)MIMEType { +#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]; + [_MIMETypeToParserClassesRegularExpressions addObject:expressionAndClass]; +} + +#endif + +- (id)parserForMIMEType:(NSString *)MIMEType { Class parserClass = [self parserClassForMIMEType:MIMEType]; if (parserClass) { return [[[parserClass alloc] init] autorelease]; } - + return nil; } - (void)autoconfigure { Class parserClass = nil; - + // JSON - NSSet* JSONParserClassNames = [NSSet setWithObjects:@"RKJSONParserJSONKit", @"RKJSONParserYAJL", @"RKJSONParserSBJSON", @"RKJSONParserNXJSON", nil]; - for (NSString* parserClassName in JSONParserClassNames) { + NSSet *JSONParserClassNames = [NSSet setWithObjects:@"RKJSONParserJSONKit", @"RKJSONParserYAJL", @"RKJSONParserSBJSON", @"RKJSONParserNXJSON", nil]; + for (NSString *parserClassName in JSONParserClassNames) { parserClass = NSClassFromString(parserClassName); if (parserClass) { [self setParserClass:parserClass forMIMEType:RKMIMETypeJSON]; break; } } - + // XML - parserClass = NSClassFromString(@"RKXMLParserLibXML"); + parserClass = NSClassFromString(@"RKXMLParserXMLReader"); if (parserClass) { [self setParserClass:parserClass forMIMEType:RKMIMETypeXML]; [self setParserClass:parserClass forMIMEType:RKMIMETypeTextXML]; diff --git a/Code/ObjectMapping/RKRouter.h b/Code/ObjectMapping/RKRouter.h index 33c1c7edf8..6d6c06383c 100644 --- a/Code/ObjectMapping/RKRouter.h +++ b/Code/ObjectMapping/RKRouter.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/20/10. -// Copyright 2010 Two Toasters -// +// 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 7e2b43c196..b779fa24b2 100644 --- a/Code/RestKit.h +++ b/Code/RestKit.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 2/19/10. -// Copyright 2010 Two Toasters -// +// 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.h b/Code/Support/NSBundle+RKAdditions.h new file mode 100644 index 0000000000..944cd9d46a --- /dev/null +++ b/Code/Support/NSBundle+RKAdditions.h @@ -0,0 +1,94 @@ +// +// NSBundle+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 2/1/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 + +#if TARGET_OS_IPHONE +#import +#endif + +/** + Provides convenience methods for accessing data in resources + within an NSBundle. + */ +@interface NSBundle (RKAdditions) + +/** + Returns an NSBundle reference to the RestKitResources.bundle file containing + RestKit specific resource assets. + + This method is a convenience wrapper for invoking + `[NSBundle bundleWithIdentifier:@"org.restkit.RestKitResources"]` + + @return An NSBundle object corresponding to the RestKitResources.bundle file. + */ ++ (NSBundle *)restKitResourcesBundle; + +/** + Returns the MIME Type for the resource identified by the specified name and file extension. + + @param name The name of the resource file. + @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. + @return The MIME Type for the resource file or nil if the file could not be located. + */ +- (NSString *)MIMETypeForResource:(NSString *)name withExtension:(NSString *)extension; + +/** + Creates and returns a data object by reading every byte from the resource identified by the specified name and file extension. + + @param name The name of the resource file. + @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. + @return A data object by reading every byte from the resource file. + */ +- (NSData *)dataWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension; + +/** + Creates and returns a string object by reading data from the resource identified by the specified name and file extension using a given encoding. + + @param name The name of the resource file. + @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. + @param encoding The encoding of the resource file. + @return A string created by reading data from the resource file using the encoding. + */ +- (NSString *)stringWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension encoding:(NSStringEncoding)encoding; + +#if TARGET_OS_IPHONE +/** + Creates and returns an image object by loading the image data from the resource identified by the specified name and file extension. + + @param name The name of the resource file. + @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. + @return A new image object for the specified file, or nil if the method could not initialize the image from the specified file. + */ +- (UIImage *)imageWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension; +#endif + +/** + Creates and returns an object representation of the data from the resource identified by the specified name and file extension by reading the + data as a string and parsing it using a parser appropriate for the MIME Type of the file. + + @param name The name of the resource file. + @param extension If extension is an empty string or nil, the extension is assumed not to exist and the file is the first file encountered that exactly matches name. + @return A new image object for the specified file, or nil if the method could not initialize the image from the specified file. + @see RKParserRegistry + */ +- (id)parsedObjectWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension; + +@end diff --git a/Code/Support/NSBundle+RKAdditions.m b/Code/Support/NSBundle+RKAdditions.m new file mode 100644 index 0000000000..eed3412e57 --- /dev/null +++ b/Code/Support/NSBundle+RKAdditions.m @@ -0,0 +1,110 @@ +// +// NSBundle+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 2/1/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 "NSBundle+RKAdditions.h" +#import "NSString+RKAdditions.h" +#import "UIImage+RKAdditions.h" +#import "RKLog.h" +#import "RKParser.h" +#import "RKParserRegistry.h" + +@implementation NSBundle (RKAdditions) + ++ (NSBundle *)restKitResourcesBundle { + static BOOL searchedForBundle = NO; + + if (! searchedForBundle) { + NSString *path = [[NSBundle mainBundle] pathForResource:@"RestKitResources" ofType:@"bundle"]; + searchedForBundle = YES; + NSBundle *resourcesBundle = [NSBundle bundleWithPath:path]; + if (! resourcesBundle) RKLogWarning(@"Unable to find RestKitResources.bundle in your project. Did you forget to add it?"); + return resourcesBundle; + } + + return [NSBundle bundleWithIdentifier:@"org.restkit.RestKitResources"]; +} + +- (NSString *)MIMETypeForResource:(NSString *)name withExtension:(NSString *)extension { + NSString *resourcePath = [self pathForResource:name ofType:extension]; + if (resourcePath) { + return [resourcePath MIMETypeForPathExtension]; + } + + return nil; +} + +- (NSData *)dataWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension { + NSString *resourcePath = [self pathForResource:name ofType:extension]; + if (! resourcePath) { + RKLogWarning(@"%@ Failed to locate Resource with name '%@' and extension '%@': File Not Found.", self, resourcePath, extension); + return nil; + } + + return [NSData dataWithContentsOfFile:resourcePath]; +} + +- (NSString *)stringWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension encoding:(NSStringEncoding)encoding { + NSError* error = nil; + NSString *resourcePath = [self pathForResource:name ofType:extension]; + if (! resourcePath) { + RKLogWarning(@"%@ Failed to locate Resource with name '%@' and extension '%@': File Not Found.", self, resourcePath, extension); + return nil; + } + + NSString* fixtureData = [NSString stringWithContentsOfFile:resourcePath encoding:encoding error:&error]; + if (fixtureData == nil && error) { + RKLogWarning(@"Failed to read "); + } + + return fixtureData; +} + +#if TARGET_OS_IPHONE +- (UIImage *)imageWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension { + NSString *resourcePath = [self pathForResource:name ofType:extension]; + if (! resourcePath) { + RKLogWarning(@"%@ Failed to locate Resource with name '%@' and extension '%@': File Not Found.", self, resourcePath, extension); + return nil; + } + + return [UIImage imageWithContentsOfResolutionIndependentFile:resourcePath]; +} +#endif + +- (id)parsedObjectWithContentsOfResource:(NSString *)name withExtension:(NSString *)extension { + NSError* error = nil; + NSString* resourceContents = [self stringWithContentsOfResource:name withExtension:extension encoding:NSUTF8StringEncoding]; + NSString* MIMEType = [self MIMETypeForResource:name withExtension:extension]; + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:MIMEType]; + if (! parser) { + RKLogError(@"%@ Unable to parse Resource with name '%@' and extension '%@': failed to find parser registered to handle MIME Type '%@'", self, name, extension, MIMEType); + return nil; + } + + id object = [parser objectFromString:resourceContents error:&error]; + if (object == nil) { + RKLogCritical(@"%@ Failed to parse resource with name '%@' and extension '%@'. Error: %@", self, name, extension, [error localizedDescription]); + return nil; + } + + return object; +} + +@end diff --git a/Code/Support/NSDictionary+RKAdditions.h b/Code/Support/NSDictionary+RKAdditions.h index 8e2f4e68cd..44fe6653c5 100644 --- a/Code/Support/NSDictionary+RKAdditions.h +++ b/Code/Support/NSDictionary+RKAdditions.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters -// +// 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,6 +20,9 @@ #import +/** + Provides useful additions to the NSDictionary interface. + */ @interface NSDictionary (RKAdditions) /** @@ -29,15 +32,27 @@ + (id)dictionaryWithKeysAndObjects:(id)firstKey, ... NS_REQUIRES_NIL_TERMINATION; /** - Strips out any percent escapes (such as %20) from the receiving dictionary's key and objects. + 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. */ -- (NSDictionary *)removePercentEscapesFromKeysAndObjects; +- (NSDictionary *)dictionaryByReplacingPercentEscapesInEntries; /** 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; +- (NSString *)URLEncodedString; // TODO: Deprecated.. + @end diff --git a/Code/Support/NSDictionary+RKAdditions.m b/Code/Support/NSDictionary+RKAdditions.m index 6158fcbfdb..dc5d79454a 100644 --- a/Code/Support/NSDictionary+RKAdditions.m +++ b/Code/Support/NSDictionary+RKAdditions.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters -// +// 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,7 +19,7 @@ // #import "NSDictionary+RKAdditions.h" -#import "NSString+RestKit.h" +#import "NSString+RKAdditions.h" #import "RKFixCategoryBug.h" RK_FIX_CATEGORY_BUG(NSDictionary_RKAdditions) @@ -27,21 +27,21 @@ @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]; } -- (NSDictionary *)removePercentEscapesFromKeysAndObjects { +- (NSDictionary *)dictionaryByReplacingPercentEscapesInEntries { NSMutableDictionary *results = [NSMutableDictionary dictionaryWithCapacity:[self count]]; [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { @@ -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]; } @@ -81,4 +81,42 @@ + (NSDictionary *)dictionaryWithURLEncodedString:(NSString *)URLEncodedString { return queryComponents; } +- (void)URLEncodePart:(NSMutableArray*)parts path:(NSString*)path value:(id)value { + NSString *encodedPart = [[value description] stringByAddingURLEncoding]; + [parts addObject:[NSString stringWithFormat: @"%@=%@", path, encodedPart]]; +} + +- (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) { + 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]; + } + else { + [self URLEncodePart:parts path:path value:value]; + } + }]; +} + +- (NSString *)stringWithURLEncodedEntries { + NSMutableArray* parts = [NSMutableArray array]; + [self URLEncodeParts:parts path:nil]; + return [parts componentsJoinedByString:@"&"]; +} + +- (NSString *)URLEncodedString { + return [self stringWithURLEncodedEntries]; +} + @end diff --git a/Code/Support/NSString+RestKit.h b/Code/Support/NSString+RKAdditions.h similarity index 81% rename from Code/Support/NSString+RestKit.h rename to Code/Support/NSString+RKAdditions.h index 9d74d889e4..b5a21bbc7b 100644 --- a/Code/Support/NSString+RestKit.h +++ b/Code/Support/NSString+RKAdditions.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/15/11. -// Copyright 2011 Two Toasters -// +// 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,31 +24,30 @@ A library of helpful additions to the NSString class to simplify common tasks within RestKit */ -@interface NSString (RestKit) +@interface NSString (RKAdditions) /** Returns a resource path from 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 - + *NOTE* - Assumes that the resource path does not already contain any query parameters. - @param queryParams A dictionary of query parameters to be URL encoded and appended to the resource path + @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 - @see RKPathAppendQueryParams */ -// TODO: Rename to stringByAppendingQueryDictionary: -- (NSString *)appendQueryParams:(NSDictionary*)queryParams; +- (NSString *)stringByAppendingQueryParameters:(NSDictionary *)queryParameters; +- (NSString *)appendQueryParams:(NSDictionary*)queryParams DEPRECATED_ATTRIBUTE; /** 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 @@ -57,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'. */ @@ -67,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'. @@ -81,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. @@ -96,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; @@ -107,22 +106,39 @@ - (NSString *)stringByReplacingURLEncoding; /** - Interprets the receiver as a path and returns the MIME Type for the path extension + 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. + */ +- (NSString *)stringByAppendingPathComponent:(NSString *)pathComponent isDirectory:(BOOL)isDirectory; + +/** + 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 */ - (BOOL)isIPAddress; +/** + Returns a string of the MD5 sum of the receiver. + + @return A new string containing the MD5 sum of the receiver. + */ +- (NSString *)MD5; + @end diff --git a/Code/Support/NSString+RestKit.m b/Code/Support/NSString+RKAdditions.m similarity index 67% rename from Code/Support/NSString+RestKit.m rename to Code/Support/NSString+RKAdditions.m index 6bc99fab19..b53e85d89c 100644 --- a/Code/Support/NSString+RestKit.m +++ b/Code/Support/NSString+RKAdditions.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/15/11. -// Copyright 2011 RestKit -// +// 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,22 +23,41 @@ #else #import #endif -#import "NSString+RestKit.h" -#import "RKClient.h" -#import "RKFixCategoryBug.h" +#import + #include #include -RK_FIX_CATEGORY_BUG(NSString_RestKit) +#import "NSString+RKAdditions.h" +#import "NSDictionary+RKAdditions.h" +#import "RKFixCategoryBug.h" +#import "RKPathMatcher.h" + +RK_FIX_CATEGORY_BUG(NSString_RKAdditions) -@implementation NSString (RestKit) +@implementation NSString (RKAdditions) +- (NSString *)stringByAppendingQueryParameters:(NSDictionary *)queryParameters { + if ([queryParameters count] > 0) { + return [NSString stringWithFormat:@"%@?%@", self, [queryParameters stringWithURLEncodedEntries]]; + } + return [NSString stringWithString:self]; +} + +// Deprecated - (NSString *)appendQueryParams:(NSDictionary *)queryParams { - return RKPathAppendQueryParams(self, queryParams); + return [self stringByAppendingQueryParameters:queryParams]; +} + +- (NSString *)interpolateWithObject:(id)object addingEscapes:(BOOL)addEscapes { + NSCAssert(object != NULL, @"Object provided is invalid; cannot create a path from a NULL object"); + RKPathMatcher *matcher = [RKPathMatcher matcherWithPattern:self]; + NSString *interpolatedPath = [matcher pathFromObject:object addingEscapes:addEscapes]; + return interpolatedPath; } - (NSString *)interpolateWithObject:(id)object { - return RKMakePathWithObject(self, object); + return [self interpolateWithObject:object addingEscapes:YES]; } - (NSDictionary *)queryParameters { @@ -66,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] @@ -87,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]; @@ -104,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 @""; } @@ -119,8 +138,13 @@ - (NSString *)stringByReplacingURLEncoding { return [self stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; } +- (NSDictionary *)fileExtensionsToMIMETypesDictionary { + return [NSDictionary dictionaryWithObjectsAndKeys:@"application/json", @"json", nil]; +} + - (NSString *)MIMETypeForPathExtension { - CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)[self pathExtension], NULL); + NSString *fileExtension = [self pathExtension]; + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef) fileExtension, NULL); if (uti != NULL) { CFStringRef mime = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType); CFRelease(uti); @@ -130,15 +154,41 @@ - (NSString *)MIMETypeForPathExtension { return type; } } - - return nil; + + // Consult our internal dictionary of mappings if not found + return [[self fileExtensionsToMIMETypesDictionary] valueForKey:fileExtension]; } - (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); } +- (NSString *)stringByAppendingPathComponent:(NSString *)pathComponent isDirectory:(BOOL)isDirectory { + NSString *stringWithPathComponent = [self stringByAppendingPathComponent:pathComponent]; + if (isDirectory) return [stringWithPathComponent stringByAppendingString:@"/"]; + return stringWithPathComponent; +} + +- (NSString *)MD5 { + // Create pointer to the string as UTF8 + const char* ptr = [self UTF8String]; + + // Create byte array of unsigned chars + unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH]; + + // Create 16 byte MD5 hash value, store in buffer + CC_MD5(ptr, (CC_LONG) strlen(ptr), md5Buffer); + + // Convert MD5 value in the 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; +} + @end diff --git a/Code/Support/NSURL+RestKit.h b/Code/Support/NSURL+RKAdditions.h similarity index 86% rename from Code/Support/NSURL+RestKit.h rename to Code/Support/NSURL+RKAdditions.h index 3551849cec..dc2c9fbe20 100644 --- a/Code/Support/NSURL+RestKit.h +++ b/Code/Support/NSURL+RKAdditions.h @@ -1,16 +1,16 @@ // -// NSURL+RestKit.h +// NSURL+RKAdditions.h // RestKit // // Created by Blake Watters on 10/11/11. -// Copyright 2011 RestKit -// +// 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,20 +20,20 @@ #import -@interface NSURL (RestKit) +@interface NSURL (RKAdditions) /** Returns the query portion of the URL as a dictionary */ -- (NSDictionary *)queryDictionary; +- (NSDictionary *)queryParameters; /** 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+RestKit.m b/Code/Support/NSURL+RKAdditions.m similarity index 76% rename from Code/Support/NSURL+RestKit.m rename to Code/Support/NSURL+RKAdditions.m index 80ce2b38a6..ee937f8375 100644 --- a/Code/Support/NSURL+RestKit.m +++ b/Code/Support/NSURL+RKAdditions.m @@ -1,16 +1,16 @@ // -// NSURL+RestKit.h +// NSURL+RKAdditions.h // RestKit // // Created by Blake Watters on 10/11/11. -// Copyright 2011 RestKit -// +// 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. @@ -18,16 +18,16 @@ // limitations under the License. // -#import "NSURL+RestKit.h" +#import "NSURL+RKAdditions.h" #import "NSDictionary+RKAdditions.h" #import "RKFixCategoryBug.h" -#import "NSString+RestKit.h" +#import "NSString+RKAdditions.h" -RK_FIX_CATEGORY_BUG(NSURL_RestKit) +RK_FIX_CATEGORY_BUG(NSURL_RKAdditions) -@implementation NSURL (RestKit) +@implementation NSURL (RKAdditions) -- (NSDictionary *)queryDictionary { +- (NSDictionary *)queryParameters { return [NSDictionary dictionaryWithURLEncodedString:self.query]; } diff --git a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h index 22347f56de..e283ec978f 100644 --- a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h +++ b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/14/11. -// Copyright 2011 Two Toasters -// +// 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 29aeb1243a..56f6ebf756 100644 --- a/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m +++ b/Code/Support/Parsers/JSON/RKJSONParserJSONKit.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 3/16/10. -// Copyright 2010 Two Toasters -// +// 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,17 +20,24 @@ #import "RKJSONParserJSONKit.h" #import "JSONKit.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitSupportParsers + // TODO: JSONKit serializer instance should be reused to enable leverage -// the internal cacheing capabilities from the JSONKit serializer +// the internal caching capabilities from the JSONKit serializer @implementation RKJSONParserJSONKit - (NSDictionary*)objectFromString:(NSString*)string error:(NSError**)error { + 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/RKXMLParserLibXML.h b/Code/Support/Parsers/XML/RKXMLParserLibXML.h deleted file mode 100644 index 2cddd11fae..0000000000 --- a/Code/Support/Parsers/XML/RKXMLParserLibXML.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// RKXMLParser.h -// -// Created by Jeremy Ellison on 2011-02-28. -// Copyright 2011 RestKit -// -// 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 -#import "RKParser.h" - -/** - This is a dead simple XML parser that uses libxml2 to parse an XML document - into a dictionary. It is designed specifically for use with RestKit. It - does not support any fanciness like Namespaces, DTDs, or other nonsense. - It handles text nodes, attributes, and nesting structures and builds key-value - coding compliant representations of the XML structure. - - It currently does not support XML generation -- only parsing. - */ -@interface RKXMLParserLibXML : NSObject { -} - -- (NSDictionary *)parseXML:(NSString *)XML; - -@end diff --git a/Code/Support/Parsers/XML/RKXMLParserLibXML.m b/Code/Support/Parsers/XML/RKXMLParserLibXML.m deleted file mode 100644 index 4e720dddd7..0000000000 --- a/Code/Support/Parsers/XML/RKXMLParserLibXML.m +++ /dev/null @@ -1,160 +0,0 @@ -// -// RKXMLParserLibXML.m -// -// Created by Jeremy Ellison on 2011-02-28. -// Copyright 2011 RestKit -// -// 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 -#import "RKXMLParserLibXML.h" - -@implementation RKXMLParserLibXML - -- (id)parseNode:(xmlNode*)node { - NSMutableArray* nodes = [NSMutableArray array]; - NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; - - xmlNode* currentNode = NULL; - for (currentNode = node; currentNode; currentNode = currentNode->next) { - if (currentNode->type == XML_ELEMENT_NODE) { - NSString* nodeName = [NSString stringWithCString:(char*)currentNode->name encoding:NSUTF8StringEncoding]; - id val = [self parseNode:currentNode->children]; - if ([val isKindOfClass:[NSString class]]) { - id oldVal = [attrs valueForKey:nodeName]; - if (nil == oldVal) { - // Assume that empty strings are irrelevant and go for an attribute-collection instead - if ([val length] == 0) { - val = [NSMutableDictionary dictionary]; - attrs = [NSMutableDictionary dictionary]; - oldVal = [attrs valueForKey:nodeName]; - NSMutableDictionary* elem = [NSMutableDictionary dictionaryWithObject:val forKey:nodeName]; - [nodes addObject:elem]; - } else { - [attrs setValue:val forKey:nodeName]; - } - } else if ([oldVal isKindOfClass:[NSMutableArray class]]) { - [oldVal addObject:val]; - } else { - NSMutableArray* array = [NSMutableArray arrayWithObjects:oldVal, val, nil]; - [attrs setValue:array forKey:nodeName]; - } - - // Only add attributes to nodes if there actually is one. - if (![nodes containsObject:attrs] && [attrs count] > 0) { - [nodes addObject:attrs]; - } - } else { - NSMutableDictionary* elem = [NSMutableDictionary dictionaryWithObject:val forKey:nodeName]; - [nodes addObject:elem]; - } - xmlElement* element = (xmlElement*)currentNode; - xmlAttribute* currentAttribute = NULL; - for (currentAttribute = (xmlAttribute*)element->attributes; currentAttribute; currentAttribute = (xmlAttribute*)currentAttribute->next) { - NSString* name = [NSString stringWithCString:(char*)currentAttribute->name encoding:NSUTF8StringEncoding]; - xmlChar* str = xmlNodeGetContent((xmlNode*)currentAttribute); - NSString* value = [NSString stringWithCString:(char*)str encoding:NSUTF8StringEncoding]; - xmlFree(str); - [attrs setValue:value forKey:name]; - if ([val isKindOfClass:[NSDictionary class]]) { - // Add attributes as properties of the class - [val setObject:value forKey:name]; - } else if (![nodes containsObject:attrs]) { - // Only add attributes to nodes if there actually is one. - [nodes addObject:attrs]; - } - } - } else if (currentNode->type == XML_TEXT_NODE || currentNode->type == XML_CDATA_SECTION_NODE) { - xmlChar* str = xmlNodeGetContent(currentNode); - NSString* part = [[NSString stringWithCString:(const char*)str encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if ([part length] > 0) { - [nodes addObject:part]; - } - xmlFree(str); - } - } - if ([nodes count] == 1) { - return [nodes objectAtIndex:0]; - } - if ([nodes count] == 0) { - return @""; - } - if (YES || [nodes containsObject:attrs]) { - // We have both attributes and children. merge everything together. - NSMutableDictionary* results = [NSMutableDictionary dictionary]; - for (NSDictionary* dict in nodes) { - for (NSString* key in dict) { - id value = [dict valueForKey:key]; - id currentValue = [results valueForKey:key]; - if (nil == currentValue) { - [results setValue:value forKey:key]; - } else if ([currentValue isKindOfClass:[NSMutableArray class]]) { - [currentValue addObject:value]; - } else { - NSMutableArray* array = [NSMutableArray arrayWithObjects: currentValue, value, nil]; - [results setValue:array forKey:key]; - } - } - } - return results; - } - return nodes; -} - -- (NSDictionary*)parseXML:(NSString*)xml { - xmlParserCtxtPtr ctxt; /* the parser context */ - xmlDocPtr doc; /* the resulting document tree */ - id result = nil;; - - /* create a parser context */ - ctxt = xmlNewParserCtxt(); - if (ctxt == NULL) { - fprintf(stderr, "Failed to allocate parser context\n"); - return nil; - } - /* Parse the string. */ - const char* buffer = [xml cStringUsingEncoding:NSUTF8StringEncoding]; - doc = xmlParseMemory(buffer, (int) strlen(buffer)); - - /* check if parsing suceeded */ - if (doc == NULL) { - fprintf(stderr, "Failed to parse\n"); - } else { - /* check if validation suceeded */ - if (ctxt->valid == 0) { - fprintf(stderr, "Failed to validate\n"); - } - - /* Parse Doc into Dict */ - result = [self parseNode:doc->xmlRootNode]; - - /* free up the resulting document */ - xmlFreeDoc(doc); - } - /* free up the parser context */ - xmlFreeParserCtxt(ctxt); - return result; -} - -- (id)objectFromString:(NSString*)string error:(NSError **)error { - // TODO: Add error handling... - return [self parseXML:string]; -} - -- (NSString*)stringFromObject:(id)object error:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - return nil; -} - -@end \ No newline at end of file diff --git a/Code/Support/Parsers/XML/RKXMLParserXMLReader.h b/Code/Support/Parsers/XML/RKXMLParserXMLReader.h new file mode 100644 index 0000000000..f4aa125fd8 --- /dev/null +++ b/Code/Support/Parsers/XML/RKXMLParserXMLReader.h @@ -0,0 +1,32 @@ +// +// RKXMLParserXMLReader.h +// RestKit +// +// Created by Christopher Swasey on 1/24/12. +// Copyright (c) 2012 GateGuru. All rights reserved. +// + +/** + 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. + */ + +#import "XMLReader.h" +#import "RKParser.h" + +@interface RKXMLParserXMLReader : NSObject { + +} + +@end diff --git a/Code/Support/Parsers/XML/RKXMLParserXMLReader.m b/Code/Support/Parsers/XML/RKXMLParserXMLReader.m new file mode 100644 index 0000000000..ed9a7dfc80 --- /dev/null +++ b/Code/Support/Parsers/XML/RKXMLParserXMLReader.m @@ -0,0 +1,22 @@ +// +// RKXMLParserXMLReader.m +// RestKit +// +// Created by Christopher Swasey on 1/24/12. +// Copyright (c) 2012 GateGuru. All rights reserved. +// + +#import "RKXMLParserXMLReader.h" + +@implementation RKXMLParserXMLReader + +- (id)objectFromString:(NSString*)string error:(NSError**)error { + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [XMLReader dictionaryForXMLData:data error:error]; +} + +- (NSString*)stringFromObject:(id)object error:(NSError**)error { + return nil; +} + +@end diff --git a/Code/Support/RKAlert.h b/Code/Support/RKAlert.h index b4627e6748..00e7ab49e8 100644 --- a/Code/Support/RKAlert.h +++ b/Code/Support/RKAlert.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/10/11. -// Copyright 2011 Two Toasters -// +// 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 4e93231239..c6ed0f64eb 100644 --- a/Code/Support/RKAlert.m +++ b/Code/Support/RKAlert.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 4/10/11. -// Copyright 2011 Two Toasters -// +// 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 new file mode 100644 index 0000000000..1b5d590fe2 --- /dev/null +++ b/Code/Support/RKCache.h @@ -0,0 +1,38 @@ +// +// RKCache.h +// RestKit +// +// Created by Jeff Arena on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@interface RKCache : NSObject { + NSString* _cachePath; + NSRecursiveLock* _cacheLock; +} + +@property (nonatomic, readonly) NSString* cachePath; + +- (id)initWithPath:(NSString*)cachePath subDirectories:(NSArray*)subDirectories; +- (BOOL)hasEntry:(NSString*)cacheKey; +- (void)invalidateEntry:(NSString*)cacheKey; +- (void)invalidateSubDirectory:(NSString*)subDirectory; +- (void)invalidateAll; +- (void)writeDictionary:(NSDictionary*)dictionary withCacheKey:(NSString*)cacheKey; +- (void)writeData:(NSData*)data withCacheKey:(NSString*)cacheKey; +- (NSDictionary*)dictionaryForCacheKey:(NSString*)cacheKey ; +- (NSData*)dataForCacheKey:(NSString*)cacheKey; + +@end diff --git a/Code/Support/RKCache.m b/Code/Support/RKCache.m new file mode 100644 index 0000000000..cc10686e17 --- /dev/null +++ b/Code/Support/RKCache.m @@ -0,0 +1,233 @@ +// +// RKCache.h +// RestKit +// +// Created by Jeff Arena on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKCache.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitSupport + +@implementation RKCache + +- (id)initWithPath:(NSString*)cachePath subDirectories:(NSArray*)subDirectories { + self = [super init]; + if (self) { + _cachePath = [cachePath copy]; + _cacheLock = [[NSRecursiveLock alloc] init]; + + 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: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 { + if (!isDirectory) { + RKLogWarning(@"Skipped creation of cache directory as non-directory file exists at path: %@", path); + } + } + } + } + return self; +} + +- (void)dealloc { + [_cachePath release]; + _cachePath = nil; + [_cacheLock release]; + _cacheLock = nil; + [super dealloc]; +} + +- (NSString*)cachePath { + return _cachePath; +} + +- (NSString*)pathForCacheKey:(NSString*)cacheKey { + [_cacheLock lock]; + NSString* pathForCacheKey = [_cachePath stringByAppendingPathComponent:cacheKey]; + [_cacheLock unlock]; + RKLogTrace(@"Found cachePath '%@' for %@", pathForCacheKey, cacheKey); + 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]; + RKLogTrace(@"Determined hasEntry: %@ => %@", cacheKey, hasEntry ? @"YES" : @"NO"); + return hasEntry; +} + +- (void)writeDictionary:(NSDictionary*)dictionary withCacheKey:(NSString*)cacheKey { + if (dictionary) { + [_cacheLock lock]; + RKLogTrace(@"Writing dictionary to cache key: '%@'", cacheKey); + BOOL success = [dictionary writeToFile:[self pathForCacheKey:cacheKey] atomically:YES]; + if (success) { + RKLogTrace(@"Wrote cached dictionary to cacheKey '%@'", cacheKey); + } else { + RKLogError(@"Failed to write cached dictionary to cacheKey '%@'", cacheKey); + } + [_cacheLock unlock]; + } +} + +- (void)writeData:(NSData*)data withCacheKey:(NSString*)cacheKey { + if (data) { + [_cacheLock lock]; + NSString* cachePath = [self pathForCacheKey:cacheKey]; + if (cachePath) { + NSError* error = nil; + BOOL success = [data writeToFile:cachePath options:NSDataWritingAtomic error:&error]; + if (success) { + RKLogTrace(@"Wrote cached data to path '%@'", cachePath); + } else { + RKLogError(@"Failed to write cached data to path '%@': %@", cachePath, [error localizedDescription]); + } + } + [_cacheLock unlock]; + } +} + +- (NSDictionary*)dictionaryForCacheKey:(NSString*)cacheKey { + [_cacheLock lock]; + NSDictionary* dictionary = nil; + NSString* cachePath = [self pathForCacheKey:cacheKey]; + if (cachePath) { + dictionary = [NSDictionary dictionaryWithContentsOfFile:cachePath]; + if (dictionary) { + RKLogDebug(@"Read cached dictionary '%@' from cachePath '%@' for '%@'", dictionary, cachePath, cacheKey); + } else { + RKLogDebug(@"Read nil cached dictionary from cachePath '%@' for '%@'", cachePath, cacheKey); + } + } else { + RKLogDebug(@"Unable to read cached dictionary for '%@': cachePath not found", cacheKey); + } + [_cacheLock unlock]; + return dictionary; +} + +- (NSData*)dataForCacheKey:(NSString*)cacheKey { + [_cacheLock lock]; + NSData* data = nil; + NSString* cachePath = [self pathForCacheKey:cacheKey]; + 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; +} + +- (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]; + RKLogTrace(@"Removed cache entry at path '%@' for '%@'", cachePath, cacheKey); + } + [_cacheLock unlock]; +} + +- (void)invalidateSubDirectory:(NSString*)subDirectory { + [_cacheLock lock]; + 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]; +} + +- (void)invalidateAll { + [_cacheLock lock]; + 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]; +} + +@end diff --git a/Code/Support/RKDirectory.h b/Code/Support/RKDirectory.h index 6c6d17aa5c..dfd0b46ff6 100644 --- a/Code/Support/RKDirectory.h +++ b/Code/Support/RKDirectory.h @@ -3,13 +3,13 @@ // RestKit // // Created by Blake Watters on 12/9/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #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 @@ -18,13 +18,28 @@ Returns the path to the Application Data directory for the executing application. On iOS, this is a sandboxed path specific for the executing application. On OS X, this is an application specific path under NSApplicationSupportDirectory (i.e. ~/Application Support). + + @return The full path to the application data directory. */ + (NSString *)applicationDataDirectory; /** 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; +/** + Ensures that a directory exists at a given path by checking for the existence + of the directory and creating it if it does not exist. + + @param path The path to ensure a directory exists at. + @param error On input, a pointer to an error object. + @returns A Boolean value indicating if the directory exists. + */ ++ (BOOL)ensureDirectoryExistsAtPath:(NSString *)path error:(NSError **)error; + @end diff --git a/Code/Support/RKDirectory.m b/Code/Support/RKDirectory.m index 4089283692..5da5deab47 100644 --- a/Code/Support/RKDirectory.m +++ b/Code/Support/RKDirectory.m @@ -3,78 +3,59 @@ // RestKit // // Created by Blake Watters on 12/9/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKDirectory.h" +#import "NSBundle+RKAdditions.h" #import "RKLog.h" @implementation RKDirectory -+ (NSString *)executableName { - NSString *executableName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleExecutable"]; ++ (NSString *)executableName +{ + NSString *executableName = [[[NSBundle mainBundle] executablePath] lastPathComponent]; if (nil == executableName) { RKLogWarning(@"Unable to determine CFBundleExecutable: storing data under RestKit directory name."); executableName = @"RestKit"; } - + return executableName; } -+ (NSString *)applicationDataDirectory { ++ (NSString *)applicationDataDirectory +{ #if TARGET_OS_IPHONE - + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; - if (basePath) { - // In unit tests the Documents/ path may not exist - if(! [[NSFileManager defaultManager] fileExistsAtPath:basePath]) { - NSError* error = nil; - - if(! [[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:NO attributes:nil error:&error]) { - NSLog(@"%@", error); - } - } - - return basePath; - } - - return nil; - + return ([paths count] > 0) ? [paths objectAtIndex:0] : nil; + #else - - NSFileManager* sharedFM = [NSFileManager defaultManager]; - - NSArray* possibleURLs = [sharedFM URLsForDirectory:NSApplicationSupportDirectory + + NSFileManager *sharedFM = [NSFileManager defaultManager]; + + NSArray *possibleURLs = [sharedFM URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; - NSURL* appSupportDir = nil; - NSURL* appDirectory = nil; - + NSURL *appSupportDir = nil; + NSURL *appDirectory = nil; + if ([possibleURLs count] >= 1) { appSupportDir = [possibleURLs objectAtIndex:0]; } - + if (appSupportDir) { NSString *executableName = [RKDirectory executableName]; appDirectory = [appSupportDir URLByAppendingPathComponent:executableName]; - - - if(![sharedFM fileExistsAtPath:[appDirectory path]]) { - NSError* error = nil; - - if(![sharedFM createDirectoryAtURL:appDirectory withIntermediateDirectories:NO attributes:nil error:&error]) { - NSLog(@"%@", error); - } - } return [appDirectory path]; } - + return nil; #endif } -+ (NSString *)cachesDirectory { -#if TARGET_OS_IPHONE ++ (NSString *)cachesDirectory +{ +#if TARGET_OS_IPHONE return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; #else NSString *path = nil; @@ -82,9 +63,30 @@ + (NSString *)cachesDirectory { if ([paths count]) { path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[RKDirectory executableName]]; } - + return path; #endif } ++ (BOOL)ensureDirectoryExistsAtPath:(NSString *)path error:(NSError **)error +{ + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) { + if (isDirectory) { + // Exists at a path and is a directory, we're good + if (error) *error = nil; + return YES; + } + } + + // Create the directory and any intermediates + NSError *errorReference = (error == nil) ? nil : *error; + if (! [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&errorReference]) { + RKLogError(@"Failed to create requested directory at path '%@': %@", path, errorReference); + return NO; + } + + return YES; +} + @end diff --git a/Code/Support/RKDotNetDateFormatter.h b/Code/Support/RKDotNetDateFormatter.h index 031209f50c..3e8466e7fb 100644 --- a/Code/Support/RKDotNetDateFormatter.h +++ b/Code/Support/RKDotNetDateFormatter.h @@ -3,7 +3,19 @@ // RestKit // // Created by Greg Combs on 9/8/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// 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 @@ -21,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 @@ -30,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: @@ -48,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 @@ -67,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 7987469258..20685ef033 100644 --- a/Code/Support/RKDotNetDateFormatter.m +++ b/Code/Support/RKDotNetDateFormatter.m @@ -1,9 +1,21 @@ // -// RKDotNetDateFormatter.m +// RKDotNetDateFormatter.h // RestKit // // Created by Greg Combs on 9/8/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// 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 "RKDotNetDateFormatter.h" @@ -53,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/Errors.h b/Code/Support/RKErrors.h similarity index 52% rename from Code/Support/Errors.h rename to Code/Support/RKErrors.h index 0f5006b50f..8e279463f7 100644 --- a/Code/Support/Errors.h +++ b/Code/Support/RKErrors.h @@ -1,16 +1,16 @@ // -// Errors.h +// RKErrors.h // RestKit // // Created by Blake Watters on 3/25/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,15 +18,36 @@ // limitations under the License. // -// The error domain for RestKit generated errors -extern NSString* const RKRestKitErrorDomain; +#import -extern NSString* const RKObjectMapperErrorObjectsKey; +/** @name Error Domain & Codes */ + +// The error domain for RestKit generated errors +extern NSString* const RKErrorDomain; typedef enum { - RKObjectLoaderRemoteSystemError = 1, - RKRequestBaseURLOfflineError = 2, + RKObjectLoaderRemoteSystemError = 1, + RKRequestBaseURLOfflineError = 2, RKRequestUnexpectedResponseError = 3, RKObjectLoaderUnexpectedResponseError = 4, RKRequestConnectionTimeoutError = 5 } RKRestKitError; + +/** @name Error Constants */ + +/** + The key RestKit generated errors will appear at within an NSNotification + indicating an error + */ +extern NSString* const RKErrorNotificationErrorKey; + +/** + When RestKit constructs an NSError object from one or more RKErrorMessage + (or other object mapped error representations), the userInfo of the NSError + object will be populated with an array of the underlying error objects. + + These underlying errors can be accessed via RKObjectMapperErrorObjectsKey key. + + @see RKObjectMappingResult + */ +extern NSString* const RKObjectMapperErrorObjectsKey; diff --git a/Code/Support/Errors.m b/Code/Support/RKErrors.m similarity index 73% rename from Code/Support/Errors.m rename to Code/Support/RKErrors.m index 1c0f1c3a70..a6a790e7d4 100644 --- a/Code/Support/Errors.m +++ b/Code/Support/RKErrors.m @@ -1,16 +1,16 @@ // -// Errors.m +// RKErrors.m // RestKit // // Created by Blake Watters on 3/25/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,8 +18,9 @@ // limitations under the License. // -#import "Errors.h" +#import "RKErrors.h" -NSString* const RKRestKitErrorDomain = @"org.restkit.RestKit.ErrorDomain"; +NSString* const RKErrorDomain = @"org.restkit.RestKit.ErrorDomain"; -NSString* const RKObjectMapperErrorObjectsKey = @"RKObjectMapperErrorObjectsKey"; \ No newline at end of file +NSString* const RKObjectMapperErrorObjectsKey = @"RKObjectMapperErrorObjectsKey"; +NSString* const RKErrorNotificationErrorKey = @"error"; diff --git a/Code/Support/RKFixCategoryBug.h b/Code/Support/RKFixCategoryBug.h index 56ed30e1e5..7bb79094f5 100644 --- a/Code/Support/RKFixCategoryBug.h +++ b/Code/Support/RKFixCategoryBug.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/1/11. -// Copyright 2011 Two Toasters -// +// 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,12 +21,12 @@ #ifndef RestKit_RKCategoryFix_h #define RestKit_RKCategoryFix_h -/** +/* Add this macro before each category implementation, so we don't have to use -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/RKISO8601DateFormatter.h b/Code/Support/RKISO8601DateFormatter.h new file mode 100644 index 0000000000..c5020c53fe --- /dev/null +++ b/Code/Support/RKISO8601DateFormatter.h @@ -0,0 +1,13 @@ +// +// RKISO8601DateFormatter.h +// RestKit +// +// Created by Christopher Swasey on 1/20/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "ISO8601DateFormatter.h" + +@interface RKISO8601DateFormatter : ISO8601DateFormatter + +@end diff --git a/Code/Support/RKISO8601DateFormatter.m b/Code/Support/RKISO8601DateFormatter.m new file mode 100644 index 0000000000..a19357d6ea --- /dev/null +++ b/Code/Support/RKISO8601DateFormatter.m @@ -0,0 +1,13 @@ +// +// RKISO8601DateFormatter.m +// RestKit +// +// Created by Christopher Swasey on 1/20/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKISO8601DateFormatter.h" + +@implementation RKISO8601DateFormatter + +@end diff --git a/Code/Support/RKLog.h b/Code/Support/RKLog.h index bd1f04251a..0bb6b456a1 100644 --- a/Code/Support/RKLog.h +++ b/Code/Support/RKLog.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/3/11. -// Copyright 2011 Two Toasters -// +// 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,9 +80,10 @@ 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 #define RKLogLevelCritical lcl_vCritical #define RKLogLevelError lcl_vError #define RKLogLevelWarning lcl_vWarning @@ -93,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); */ @@ -110,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) \ @@ -137,20 +138,36 @@ 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 statements executed within the body of the block for the current logging component will log at the new - logging level. After the block has finished excution, the logging level is restored to its previous state. + logging level. After the block has finished execution, the logging level is restored to its previous state. */ #define RKLogWithLevelWhileExecutingBlock(_level, _block) \ RKLogToComponentWithLevelWhileExecutingBlock(RKLogComponent, _level, _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) \ + RKLogToComponentWithLevelWhileExecutingBlock(RKLogComponent, RKLogLevelOff, _block) + + /** 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 @@ -165,3 +182,38 @@ lcl_configure_by_name("App", level); Initialize the logging environment */ void RKLogInitialize(void); + + +/** + Configure RestKit logging from environment variables. + (Use Option + Command + R to set Environment Variables prior to run.) + + For example to configure the equivalent of setting the following in code: + RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); + + Define an environment variable named RKLogLevel.RestKit.Network and set its value to "Trace" + + See lcl_config_components.h for configurable RestKit logging components. + + Valid values are the following: + Default or 0 + Critical or 1 + Error or 2 + Warning or 3 + Info or 4 + Debug or 5 + 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 e9d0a4652b..ca5b06c9da 100644 --- a/Code/Support/RKLog.m +++ b/Code/Support/RKLog.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/10/11. -// Copyright 2011 Two Toasters -// +// 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,9 +21,12 @@ #import "RKLog.h" #import "lcl.h" +int RKLogLevelForString(NSString *, NSString *); + 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); @@ -31,3 +34,138 @@ void RKLogInitialize(void) { loggingInitialized = YES; } } + + +void RKLogConfigureFromEnvironment(void) +{ + NSArray *validEnvVariables = [NSArray arrayWithObjects: + @"RKLogLevel.App", + @"RKLogLevel.RestKit", + @"RKLogLevel.RestKit.CoreData", + @"RKLogLevel.RestKit.CoreData.SearchEngine", + @"RKLogLevel.RestKit.Network", + @"RKLogLevel.RestKit.Network.Cache", + @"RKLogLevel.RestKit.Network.Queue", + @"RKLogLevel.RestKit.Network.Reachability", + @"RKLogLevel.RestKit.ObjectMapping", + @"RKLogLevel.RestKit.Support", + @"RKLogLevel.RestKit.Support.Parsers", + @"RKLogLevel.RestKit.Testing", + @"RKLogLevel.RestKit.Three20", + @"RKLogLevel.RestKit.UI", + nil]; + + static NSString *logComponentPrefix = @"RKLogLevel."; + + NSDictionary *envVars = [[NSProcessInfo processInfo] environment]; + + for (NSString *envVarName in [envVars allKeys]) { + if ([envVarName hasPrefix:logComponentPrefix]) { + if (![validEnvVariables containsObject:envVarName]) { + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"The RKLogLevel Environment Variable name must be one of the following: %@", validEnvVariables] userInfo:nil]; + } + NSString *logLevel = [envVars valueForKey:envVarName]; + NSString *logComponent = [envVarName stringByReplacingOccurrencesOfString:logComponentPrefix withString:@""]; + logComponent = [logComponent stringByReplacingOccurrencesOfString:@"." withString:@"/"]; + + const char* log_component_c_str = [logComponent cStringUsingEncoding:NSUTF8StringEncoding]; + int log_level_int = RKLogLevelForString(logLevel, envVarName); + RKLogConfigureByName(log_component_c_str, log_level_int); + } + } +} + + +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:@""]; + + if ([logLevel isEqualToString:@"Off"] || + [logLevel isEqualToString:@"0"]) { + return RKLogLevelOff; + } + else if ([logLevel isEqualToString:@"Critical"] || + [logLevel isEqualToString:@"1"]) { + return RKLogLevelCritical; + } + else if ([logLevel isEqualToString:@"Error"] || + [logLevel isEqualToString:@"2"]) { + return RKLogLevelError; + } + else if ([logLevel isEqualToString:@"Warning"] || + [logLevel isEqualToString:@"3"]) { + return RKLogLevelWarning; + } + else if ([logLevel isEqualToString:@"Info"] || + [logLevel isEqualToString:@"4"]) { + return RKLogLevelInfo; + } + else if ([logLevel isEqualToString:@"Debug"] || + [logLevel isEqualToString:@"5"]) { + return RKLogLevelDebug; + } + else if ([logLevel isEqualToString:@"Trace"] || + [logLevel isEqualToString:@"6"]) { + return RKLogLevelTrace; + } + else if ([logLevel isEqualToString:@"Default"]) { + return RKLogLevelDefault; + } + else { + NSString *errorMessage = [NSString stringWithFormat:@"The value: \"%@\" for the environment variable: \"%@\" is invalid. \ + \nThe log level must be set to one of the following values \ + \n Default or 0 \ + \n Critical or 1 \ + \n Error or 2 \ + \n Warning or 3 \ + \n Info or 4 \ + \n Debug or 5 \ + \n Trace or 6\n", logLevel, envVarName]; + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:errorMessage userInfo:nil]; + + 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 8765f859b7..7cfdbaa300 100644 --- a/Code/Support/RKMIMETypes.h +++ b/Code/Support/RKMIMETypes.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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 f7f46665cd..818c21ec75 100644 --- a/Code/Support/RKMIMETypes.m +++ b/Code/Support/RKMIMETypes.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// +// 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.h b/Code/Support/RKMutableBlockDictionary.h new file mode 100644 index 0000000000..75050988bb --- /dev/null +++ b/Code/Support/RKMutableBlockDictionary.h @@ -0,0 +1,45 @@ +// +// RKMutableBlockDictionary.h +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +/** + A dictionary capable of storing dynamic values provided + as a Objective-C block. Otherwise identical in functionality + to a vanilla NSMutableDictionary + */ +@interface RKMutableBlockDictionary : NSMutableDictionary { + @private + NSMutableDictionary *_mutableDictionary; +} + +/** + Assigns a block as the value for a key in the dictionary. This allows you + to implement simple logic using key-value coding semantics within the dictionary. + + When valueForKey: is invoked on the dictionary for a key with a block value, the + block will be evaluated and the result returned. + + @param block An Objective-C block returning an id and accepting no parameters + @param key An NSString key for setting the + */ +- (void)setValueWithBlock:(id (^)())block forKey:(NSString *)key; + +@end diff --git a/Code/Support/RKMutableBlockDictionary.m b/Code/Support/RKMutableBlockDictionary.m new file mode 100644 index 0000000000..a2dbbf58b1 --- /dev/null +++ b/Code/Support/RKMutableBlockDictionary.m @@ -0,0 +1,145 @@ +// +// RKMutableBlockDictionary.m +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKMutableBlockDictionary.h" + +typedef id (^RKMutableBlockDictionaryValueBlock)(); + +@interface RKMutableBlockDictionaryBlockValue : NSObject { + RKMutableBlockDictionaryValueBlock _executionBlock; +} + +@property (nonatomic, copy) RKMutableBlockDictionaryValueBlock executionBlock; + ++ (id)valueWithBlock:(RKMutableBlockDictionaryValueBlock)executionBlock; + +@end + +@implementation RKMutableBlockDictionaryBlockValue + +@synthesize executionBlock = _executionBlock; + ++ (id)valueWithBlock:(RKMutableBlockDictionaryValueBlock)executionBlock { + RKMutableBlockDictionaryBlockValue* value = [[self new] autorelease]; + value.executionBlock = executionBlock; + + return value; +} + +- (void)setExecutionBlock:(RKMutableBlockDictionaryValueBlock)executionBlock { + if (_executionBlock) { + Block_release(_executionBlock); + _executionBlock = nil; + } + _executionBlock = Block_copy(executionBlock); +} + +- (void)dealloc { + if (_executionBlock) { + Block_release(_executionBlock); + _executionBlock = nil; + } + [super dealloc]; +} + +@end + +@implementation RKMutableBlockDictionary + +- (id)init { + return [self initWithCapacity:0]; +} + +- (id)initWithCapacity:(NSUInteger)capacity { + self = [super init]; + if (self != nil) { + _mutableDictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity]; + } + + return self; +} + +- (void)dealloc { + [_mutableDictionary release]; + [super dealloc]; +} + +- (id)copy { + return [self mutableCopy]; +} + +- (void)setObject:(id)anObject forKey:(id)aKey { + [_mutableDictionary setObject:anObject forKey:aKey]; +} + +- (void)removeObjectForKey:(id)aKey { + [_mutableDictionary removeObjectForKey:aKey]; +} + +- (NSUInteger)count { + return [_mutableDictionary count]; +} + +- (id)objectForKey:(id)aKey { + return [_mutableDictionary objectForKey:aKey]; +} + +- (NSEnumerator *)keyEnumerator { + return [_mutableDictionary keyEnumerator]; +} + +- (void)setValueWithBlock:(id (^)())block forKey:(NSString *)key { + RKMutableBlockDictionaryBlockValue* blockValue = [RKMutableBlockDictionaryBlockValue valueWithBlock:block]; + [self setObject:blockValue forKey:key]; +} + +- (id)valueForKey:(NSString *)key { + id value = [self objectForKey:key]; + if (value) { + if ([value isKindOfClass:[RKMutableBlockDictionaryBlockValue class]]) { + RKMutableBlockDictionaryBlockValue *blockValue = (RKMutableBlockDictionaryBlockValue *)value; + return blockValue.executionBlock(); + } + + return value; + } + + return nil; +} + +- (id)valueForKeyPath:(NSString *)keyPath { + id value = [super valueForKeyPath:keyPath]; + if (value) { + if ([value isKindOfClass:[RKMutableBlockDictionaryBlockValue class]]) { + RKMutableBlockDictionaryBlockValue *blockValue = (RKMutableBlockDictionaryBlockValue *)value; + return blockValue.executionBlock(); + } + + return value; + } + + return nil; +} + +- (void)setValue:(id)value forKey:(NSString *)key { + [self setObject:value forKey:key]; +} + +@end diff --git a/Code/Support/RKOrderedDictionary.h b/Code/Support/RKOrderedDictionary.h new file mode 100644 index 0000000000..bbbc68e16d --- /dev/null +++ b/Code/Support/RKOrderedDictionary.h @@ -0,0 +1,64 @@ +// +// OrderedDictionary.h +// OrderedDictionary +// +// Created by Matt Gallagher on 19/12/08. +// Copyright 2008 Matt Gallagher. All rights reserved. +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. Permission is granted to anyone to +// use this software for any purpose, including commercial applications, and to +// alter it and redistribute it freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source +// distribution. +// + +#import + +/** + The RKOrderedDictionary class declares the programmatic interface to objects that manage mutable + associations of keys and values wherein the keys have a specific order. It adds ordered key modification + operations to the basic operations it inherits from NSMutableDictionary. + */ +// Borrowed from Matt Gallagher - http://cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html +@interface RKOrderedDictionary : NSMutableDictionary +{ + NSMutableDictionary *dictionary; + NSMutableArray *array; +} + +/** + Inserts an object into the dictionary for a given key at a specific index. + + @param anObject The object to add the dictionary. + @param aKey The key to store the value under. + @param anIndex The index in the dictionary at which to insert aKey. + */ +- (void)insertObject:(id)anObject forKey:(id)aKey atIndex:(NSUInteger)anIndex; + +/** + Returns the key within the dictionary at a given index. + + @param anIndex An index within the bounds of the array keys. + @return The key that appears at the given index. + */ +- (id)keyAtIndex:(NSUInteger)anIndex; + +/** + Returns an enumerator object that lets you access each key in the dictionary, + in reverse order. + + @return An enumerator object that lets you access each key in the dictionary + in reverse order. + */ +- (NSEnumerator *)reverseKeyEnumerator; + +@end diff --git a/Code/Support/RKOrderedDictionary.m b/Code/Support/RKOrderedDictionary.m new file mode 100644 index 0000000000..2087c598e1 --- /dev/null +++ b/Code/Support/RKOrderedDictionary.m @@ -0,0 +1,151 @@ +// +// OrderedDictionary.m +// OrderedDictionary +// +// Created by Matt Gallagher on 19/12/08. +// Copyright 2008 Matt Gallagher. All rights reserved. +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. Permission is granted to anyone to +// use this software for any purpose, including commercial applications, and to +// alter it and redistribute it freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source +// distribution. +// + +#import "RKOrderedDictionary.h" + +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; +} + +@implementation RKOrderedDictionary + +- (id)init +{ + 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; +} + +- (void)dealloc +{ + [dictionary release]; + [array release]; + [super dealloc]; +} + +- (id)copy +{ + return [self mutableCopy]; +} + +- (void)setObject:(id)anObject forKey:(id)aKey +{ + if (![dictionary objectForKey:aKey]) + { + [array addObject:aKey]; + } + [dictionary setObject:anObject forKey:aKey]; +} + +- (void)removeObjectForKey:(id)aKey +{ + [dictionary removeObjectForKey:aKey]; + [array removeObject:aKey]; +} + +- (NSUInteger)count +{ + return [dictionary count]; +} + +- (id)objectForKey:(id)aKey +{ + return [dictionary objectForKey:aKey]; +} + +- (NSEnumerator *)keyEnumerator +{ + return [array objectEnumerator]; +} + +- (NSEnumerator *)reverseKeyEnumerator +{ + 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]; +} + +- (id)keyAtIndex:(NSUInteger)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", + indentString, + RKDescriptionForObject(key, locale, level), + RKDescriptionForObject([self objectForKey:key], locale, level)]; + } + [description appendFormat:@"%@}\n", indentString]; + return description; +} + +@end diff --git a/Code/Support/RKParser.h b/Code/Support/RKParser.h index 90ea28f262..4302d767c6 100644 --- a/Code/Support/RKParser.h +++ b/Code/Support/RKParser.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 10/1/10. -// Copyright 2010 Two Toasters -// +// 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,18 +19,32 @@ // /** - * A Parser is responsible for transforming a string - * of data into a dictionary. This allows the model mapper to - * map properties using key-value coding + The RKParser protocol declares two methods that a class must implement + so that it can provide support for parsing and serializing object + representations to the RestKit framework. Parsers are required to transform + data to and from string representations and are configured via the + RKParserRegistry shared instance. */ @protocol RKParser /** - * Return a key-value coding compliant representation of a payload. - * Object attributes are encoded as a dictionary and collections - * of objects are returned as arrays. + Returns an object representation of the source string encoded in the + format provided by the parser (i.e. JSON, XML, etc). + + @param string The string representation of the object to be parsed. + @param error A pointer to an NSError object. + @return The parsed object or nil if an error occurred during parsing. + */ +- (id)objectFromString:(NSString *)string error:(NSError **)error; + +/** + Returns a string representation encoded in the format + provided by the parser (i.e. JSON, XML, etc) for the given object. + + @param object The object to be serialized. + @param A pointer to an NSError object. + @return A string representation of the serialized object or nil if an error occurred. */ -- (id)objectFromString:(NSString*)string error:(NSError**)error; -- (NSString*)stringFromObject:(id)object error:(NSError**)error; +- (NSString *)stringFromObject:(id)object error:(NSError **)error; @end diff --git a/Code/Support/RKPathMatcher.h b/Code/Support/RKPathMatcher.h index 4af8992d0a..53c5ecfb96 100755 --- a/Code/Support/RKPathMatcher.h +++ b/Code/Support/RKPathMatcher.h @@ -3,14 +3,14 @@ // RestKit // // Created by Greg Combs on 9/2/11. -// Copyright 2011 RestKit -// +// 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,85 +23,85 @@ /** 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 */ -@interface RKPathMatcher : NSObject { +@interface RKPathMatcher : NSObject { @private SOCPattern *socPattern_; NSString *sourcePath_; NSString *rootPath_; NSDictionary *queryParameters_; } -@property (retain,readonly) NSDictionary *queryParameters; +@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 9fb879dfe1..4726f499c4 100755 --- a/Code/Support/RKPathMatcher.m +++ b/Code/Support/RKPathMatcher.m @@ -3,14 +3,14 @@ // RestKit // // Created by Greg Combs on 9/2/11. -// Copyright 2011 RestKit -// +// 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 "RKPathMatcher.h" #import "SOCKit.h" -#import "NSString+RestKit.h" +#import "NSString+RKAdditions.h" #import "NSDictionary+RKAdditions.h" #import "RKLog.h" @@ -58,7 +58,7 @@ @interface RKPathMatcher() @property (nonatomic,retain) SOCPattern *socPattern; @property (nonatomic,copy) NSString *sourcePath; @property (nonatomic,copy) NSString *rootPath; -@property (retain,readwrite) NSDictionary *queryParameters; +@property (copy,readwrite) NSDictionary *queryParameters; @end @implementation RKPathMatcher @@ -67,6 +67,16 @@ @implementation RKPathMatcher @synthesize rootPath=rootPath_; @synthesize queryParameters=queryParameters_; +- (id)copyWithZone:(NSZone *)zone { + RKPathMatcher* copy = [[[self class] allocWithZone:zone] init]; + copy.socPattern = self.socPattern; + copy.sourcePath = self.sourcePath; + copy.rootPath = self.rootPath; + copy.queryParameters = self.queryParameters; + + return copy; +} + - (void)dealloc { self.socPattern = nil; self.sourcePath = nil; @@ -99,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; @@ -116,12 +126,11 @@ - (BOOL)itMatchesAndHasParsedArguments:(NSDictionary **)arguments tokenizeQueryS if (![self matches]) return NO; if (!arguments) { - RKLogWarning(@"The parsed arguments dictionary reference is nil."); return YES; } NSDictionary *extracted = [self.socPattern parameterDictionaryFromSourceString:self.rootPath]; if (extracted) - [argumentsCollection addEntriesFromDictionary:[extracted removePercentEscapesFromKeysAndObjects]]; + [argumentsCollection addEntriesFromDictionary:[extracted dictionaryByReplacingPercentEscapesInEntries]]; *arguments = argumentsCollection; return YES; } 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 b0641ebe73..918261b37b 100644 --- a/Code/Support/RKSearchEngine.h +++ b/Code/Support/RKSearchEngine.h @@ -1,16 +1,16 @@ // // RKSearchEngine.h -// Two Toasters +// RestKit // // Created by Blake Watters on 8/26/09. -// Copyright 2009 Two Toasters -// +// 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,74 +21,114 @@ #import /** - * Methods conforming to this protocol are searchable - * without specifying a list of properties to concatenate + The RKSearchable protocol declares a method for providing a searchable + textual representation of content contained within the receiver. Searchable + objects can be searched via instances of RKSearchEngine. */ @protocol RKSearchable +@required + /** - * Returns a string representation of searchable text the object exposes + Returns a string representation of searchable text contained within the receiver. + + @return A string of searchable text. */ -- (NSString*)searchableText; +- (NSString *)searchableText; @end -typedef enum _RKSearchMode { - RKSearchModeAnd, - RKSearchModeOr +/** + The mode (and/or) in which to search for tokenized text via an RKSearchEngine instance. + */ +typedef enum { + /** + The search text should be matched inclusively using AND. + Matches will include all search terms. + */ + RKSearchModeAnd, + + /** + The search text should be matched exclusively using OR. + Matches will include any search terms. + */ + RKSearchModeOr } RKSearchMode; -@interface RKSearchEngine : NSObject { - RKSearchMode _mode; - BOOL _tokenizeQuery; - BOOL _stripWhitespace; - BOOL _caseSensitive; -} +/** + An instance of RKSearchEngine provides a simple interface for searching + arbitrary objects for matching text. Searching is performed by constructing + a compound NSPredicate and evaluating a collection of candidate objects for matches. + RKSearchEngine is only suitable for searching a relatively small collection of in-memory + objects that are not backed by Core Data (see RKManagedObjectSearchEngine). + + @see RKManagedObjectSearchEngine + */ +@interface RKSearchEngine : NSObject + +///----------------------------------------------------------------------------- +/// @name Configuring Search Parameters +///----------------------------------------------------------------------------- /** - * The type of searching to perform. Can be either RKSearchModeAnd or RKSearchModeOr. - * - * Defaults to RKSearchModeOr + The type of matching to perform. Can be either RKSearchModeAnd or RKSearchModeOr. + + **Default**: RKSearchModeOr */ @property (nonatomic, assign) RKSearchMode mode; /** - * Whether or not to split the search query at whitespace boundaries or consider the string - * as a single term - * - * Defaults to YES + A Boolean value that determines if the search query should be split into subterms at whitespace boundaries. + + **Default**: YES */ @property (nonatomic, assign) BOOL tokenizeQuery; /** - * Whether or not to strip the whitespace off of the search terms before searching. This can - * prevent search misses when the terms have leading/trailing whitespace - * - * Defaults to YES + A Boolean value that determines if whitespace is to be stripped off of the search terms before searching. + + This can prevent search misses when the terms have leading/trailing whitespace. + + **Default**: YES */ -@property (nonatomic, assign) BOOL stripWhitespace; +@property (nonatomic, assign) BOOL stripsWhitespace; /** - * Whether or not to perform a case-sensitive search - * - * Defaults to NO + A Boolean value that determines if search terms should be matched case sensitively. + + **Default**: NO */ -@property (nonatomic, assign) BOOL caseSensitive; +@property (nonatomic, assign, getter = isCaseSensitive) BOOL caseSensitive; /** - * Construct a new search engine + Creates and returns a search engine object. + + @returns A search engine object. */ + (id)searchEngine; +///----------------------------------------------------------------------------- +/// @name Performing a Search +///----------------------------------------------------------------------------- + /** - * Search for a string in a collection of RKSearchable objects + Searches a collection of RKSearchable objects for the given text using the configuration of the receiver + and returns an array of objects for which a match was found. + + @return A new array containing the objects in the given collection for which a match was found. + @exception NSInvalidArgumentException Raised if any objects contained in the search collection do not + conform to the RKSearchable protocol. */ -- (NSArray*)searchFor:(NSString*)searchText inCollection:(NSArray*)collection; +- (NSArray *)searchFor:(NSString *)searchText inCollection:(NSArray *)collection; /** - * Search for a string in a collection of objects by specifying an array of - * properties (strings that correspond to the selectors of properties) that return strings + Searches a set of properties in a collection of objects for the given text using the configuration of the receiver + and returns an array of objects for which a match was found. + + @return A new array containing the objects in the given collection for which a match was found. + @exception NSInvalidArgumentException Raised if any objects contained in the search collection do not + conform to the RKSearchable protocol. */ -- (NSArray*)searchFor:(NSString*)searchText onProperties:(NSArray*)properties inCollection:(NSArray*)collection; +- (NSArray *)searchFor:(NSString *)searchText onProperties:(NSArray *)properties inCollection:(NSArray *)collection; @end diff --git a/Code/Support/RKSearchEngine.m b/Code/Support/RKSearchEngine.m index 82e5c43428..a6bfa70808 100644 --- a/Code/Support/RKSearchEngine.m +++ b/Code/Support/RKSearchEngine.m @@ -1,16 +1,16 @@ // // RKSearchEngine.m -// Two Toasters +// RestKit // // Created by Blake Watters on 8/26/09. -// Copyright 2009 Two Toasters -// +// 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,89 +22,99 @@ @implementation RKSearchEngine -@synthesize mode = _mode, tokenizeQuery = _tokenizeQuery, stripWhitespace = _stripWhitespace, caseSensitive = _caseSensitive; +@synthesize mode; +@synthesize tokenizeQuery; +@synthesize stripsWhitespace; +@synthesize caseSensitive; -+ (id)searchEngine { - return [[[RKSearchEngine alloc] init] autorelease]; ++ (id)searchEngine +{ + return [[[RKSearchEngine alloc] init] autorelease]; } -- (id)init { +- (id)init +{ self = [super init]; - if (self) { - _mode = RKSearchModeOr; - _tokenizeQuery = YES; - _stripWhitespace = YES; - _caseSensitive = NO; - } - - return self; + if (self) { + mode = RKSearchModeOr; + tokenizeQuery = YES; + stripsWhitespace = YES; + caseSensitive = NO; + } + + return self; } #pragma mark Private #pragma mark - -- (NSString*)stripWhitespaceIfNecessary:(NSString*)string { - if (_stripWhitespace) { - return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - } else { - return string; - } +- (NSString *)stripWhitespaceIfNecessary:(NSString *)string +{ + if (stripsWhitespace) { + return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + + return string; } -- (NSArray*)tokenizeOrCollect:(NSString*)string { - if (_tokenizeQuery) { - return [string componentsSeparatedByString:@" "]; - } else { - return [NSArray arrayWithObject:string]; - } +- (NSArray *)tokenizeOrCollect:(NSString *)string +{ + 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 (_caseSensitive) { - predicate = [NSPredicate predicateWithFormat:@"(%K contains %@)", property, searchTerm]; - } else { - predicate = [NSPredicate predicateWithFormat:@"(%K contains[cd] %@)", property, searchTerm]; - } - [termPredicates addObject:predicate]; - } - - // build an and 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]; +- (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]; } #pragma mark Public #pragma mark - -- (NSArray*)searchFor:(NSString*)searchText inCollection:(NSArray*)collection { - NSArray* properties = [NSArray arrayWithObject:@"searchableText"]; - return [self searchFor:searchText onProperties:properties inCollection:collection]; +- (NSArray *)searchFor:(NSString *)searchText inCollection:(NSArray *)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; - } +- (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; + } } @end diff --git a/Code/Support/Support.h b/Code/Support/Support.h index 23a30f17a7..f3242978a2 100644 --- a/Code/Support/Support.h +++ b/Code/Support/Support.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 9/30/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,11 +18,19 @@ // limitations under the License. // -#import "Errors.h" -#import "NSDictionary+RKAdditions.h" +// Load shared support code +#import "RKErrors.h" #import "RKMIMETypes.h" #import "RKLog.h" -#import "NSString+RestKit.h" #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 fedc9ba57a..50ce8a2440 100644 --- a/Code/Support/lcl_config_components.h +++ b/Code/Support/lcl_config_components.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/8/11. -// Copyright 2011 Two Toasters -// +// 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,18 +42,25 @@ // 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'. -// +// // // RestKit Logging Components // -_lcl_component(RestKit, "restkit", "RestKit") -_lcl_component(RestKitNetwork, "restkit.network", "RestKit/Network") -_lcl_component(RestKitNetworkCache, "restkit.network.cache", "RestKit/Network/Cache") -_lcl_component(RestKitNetworkQueue, "restkit.network.queue", "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(App, "app", "App") \ No newline at end of file +_lcl_component(RestKit, "restkit", "RestKit") +_lcl_component(RestKitNetwork, "restkit.network", "RestKit/Network") +_lcl_component(RestKitNetworkCache, "restkit.network.cache", "RestKit/Network/Cache") +_lcl_component(RestKitNetworkQueue, "restkit.network.queue", "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") +_lcl_component(RestKitThree20, "restkit.three20", "RestKit/Three20") +_lcl_component(RestKitUI, "restkit.ui", "RestKit/UI") +_lcl_component(RestKitTesting, "restkit.testing", "RestKit/Testing") +_lcl_component(App, "app", "App") diff --git a/Code/Support/lcl_config_extensions.h b/Code/Support/lcl_config_extensions.h index 5b9a461bc2..f9b317652c 100644 --- a/Code/Support/lcl_config_extensions.h +++ b/Code/Support/lcl_config_extensions.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/8/11. -// Copyright 2011 Two Toasters -// +// 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 456ce98d1d..44eabdcc6c 100644 --- a/Code/Support/lcl_config_logger.h +++ b/Code/Support/lcl_config_logger.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 6/8/11. -// Copyright 2011 Two Toasters -// +// 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 new file mode 100644 index 0000000000..4d62375762 --- /dev/null +++ b/Code/Testing/RKMappingTest.h @@ -0,0 +1,178 @@ +// +// RKMappingTest.h +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKObjectMappingOperation.h" +#import "RKMappingTestExpectation.h" + +/** + An RKMappingTest object provides support for unit testing + a RestKit object mapping operation by evaluation expectations + against events recorded during an object mapping operation. + */ +@interface RKMappingTest : NSObject + +///----------------------------------------------------------------------------- +/// @name Creating Tests +///----------------------------------------------------------------------------- + +/** + 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. + */ ++ (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping object:(id)sourceObject; + +/** + 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. + @return A new mapping test object for a mapping, a source object and a destination object. + */ ++ (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; + +/** + 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. + @return The receiver, initialized with mapping, sourceObject and destinationObject. + */ +- (id)initWithMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject; + +///----------------------------------------------------------------------------- +/// @name Setting Expectations +///----------------------------------------------------------------------------- + +/** + 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 + */ +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath; + +/** + 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. + @see RKObjectMappingTestExpectation + */ +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withValue:(id)value; + +/** + 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. + @see RKObjectMappingTestExpectation + */ +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath passingTest:(BOOL (^)(RKObjectAttributeMapping *mapping, id value))evaluationBlock; + +/** + 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. + @see RKObjectMappingTestExpectation + @see verifiesOnExpect + */ +- (void)addExpectation:(RKMappingTestExpectation *)expectation; + +///----------------------------------------------------------------------------- +/// @name Verifying Results +///----------------------------------------------------------------------------- + +/** + Performs the object mapping operation and records any mapping events that occur. The + mapping events can be verified against expectation through a subsequent call to verify. + + @exception NSInternalInconsistencyException Raises an + NSInternalInconsistencyException if mapping fails. + */ +- (void)performMapping; + +/** + 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. + */ +- (void)verify; + +///----------------------------------------------------------------------------- +/// @name Test Configuration +///----------------------------------------------------------------------------- + +/** + The object mapping under test. + */ +@property(nonatomic, strong, readonly) RKObjectMapping *mapping; + +/** + A key path to apply to the source object to specify the location of the root + of the data under test. Useful when testing subsets of a larger payload or + object graph. + + **Default**: nil + */ +@property(nonatomic, copy) NSString *rootKeyPath; + +/** + The source object being mapped from. + */ +@property(nonatomic, strong, readonly) id sourceObject; + +/** + 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; + +/** + A Boolean value that determines if expectations should be verified immediately + when added to the receiver. + + **Default**: NO + */ +@property(nonatomic, assign) BOOL verifiesOnExpect; + +@end diff --git a/Code/Testing/RKMappingTest.m b/Code/Testing/RKMappingTest.m new file mode 100644 index 0000000000..4b015a7371 --- /dev/null +++ b/Code/Testing/RKMappingTest.m @@ -0,0 +1,229 @@ +// +// RKMappingTest.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKMappingTest.h" + +BOOL RKObjectIsValueEqualToValue(id sourceValue, id destinationValue); + +///----------------------------------------------------------------------------- +///----------------------------------------------------------------------------- + +@interface RKMappingTestEvent : NSObject + +@property (nonatomic, strong, readonly) RKObjectAttributeMapping *mapping; +@property (nonatomic, strong, readonly) id value; + +@property (nonatomic, readonly) NSString *sourceKeyPath; +@property (nonatomic, readonly) NSString *destinationKeyPath; + ++ (RKMappingTestEvent *)eventWithMapping:(RKObjectAttributeMapping *)mapping value:(id)value; + +@end + +@interface RKMappingTestEvent () +@property (nonatomic, strong, readwrite) id value; +@property (nonatomic, strong, readwrite) RKObjectAttributeMapping *mapping; +@end + +@implementation RKMappingTestEvent + +@synthesize value; +@synthesize mapping; + ++ (RKMappingTestEvent *)eventWithMapping:(RKObjectAttributeMapping *)mapping value:(id)value { + RKMappingTestEvent *event = [RKMappingTestEvent new]; + event.value = value; + event.mapping = mapping; + + return event; +} + +- (NSString *)sourceKeyPath { + return self.mapping.sourceKeyPath; +} + +- (NSString *)destinationKeyPath { + return self.mapping.destinationKeyPath; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@: mapped sourceKeyPath '%@' => destinationKeyPath '%@' with value: %@", [self class], self.sourceKeyPath, self.destinationKeyPath, self.value]; +} + +@end + +///----------------------------------------------------------------------------- +///----------------------------------------------------------------------------- + +@interface RKMappingTest () +@property (nonatomic, strong, readwrite) RKObjectMapping *mapping; +@property (nonatomic, strong, readwrite) id sourceObject; +@property (nonatomic, strong, readwrite) id destinationObject; +@property (nonatomic, strong) NSMutableArray *expectations; +@property (nonatomic, strong) NSMutableArray *events; +@property (nonatomic, assign, getter = hasPerformedMapping) BOOL performedMapping; + +// Method Definitions for old compilers +- (void)performMapping; +- (void)verifyExpectation:(RKMappingTestExpectation *)expectation; + +@end + +@implementation RKMappingTest + +@synthesize sourceObject = _sourceObject; +@synthesize destinationObject = _destinationObject; +@synthesize mapping = _mapping; +@synthesize rootKeyPath = _rootKeyPath; +@synthesize expectations = _expectations; +@synthesize events = _events; +@synthesize verifiesOnExpect = _verifiesOnExpect; +@synthesize performedMapping = _performedMapping; + ++ (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping object:(id)sourceObject { + return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:nil]; +} + ++ (RKMappingTest *)testForMapping:(RKObjectMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { + return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:destinationObject]; +} + +- (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) { + _sourceObject = sourceObject; + _destinationObject = destinationObject; + _mapping = mapping; + _expectations = [NSMutableArray new]; + _events = [NSMutableArray new]; + _verifiesOnExpect = NO; + _performedMapping = NO; + } + + return self; +} + +- (void)addExpectation:(RKMappingTestExpectation *)expectation { + [self.expectations addObject:expectation]; + + if (self.verifiesOnExpect) { + [self performMapping]; + [self verifyExpectation:expectation]; + } +} + +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath { + [self addExpectation:[RKMappingTestExpectation expectationWithSourceKeyPath:sourceKeyPath destinationKeyPath:destinationKeyPath]]; +} + +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath withValue:(id)value { + [self addExpectation:[RKMappingTestExpectation expectationWithSourceKeyPath:sourceKeyPath destinationKeyPath:destinationKeyPath value:value]]; +} + +- (void)expectMappingFromKeyPath:(NSString *)sourceKeyPath toKeyPath:(NSString *)destinationKeyPath passingTest:(BOOL (^)(RKObjectAttributeMapping *mapping, id value))evaluationBlock { + [self addExpectation:[RKMappingTestExpectation expectationWithSourceKeyPath:sourceKeyPath destinationKeyPath:destinationKeyPath evaluationBlock:evaluationBlock]]; +} + +- (RKMappingTestEvent *)eventMatchingKeyPathsForExpectation:(RKMappingTestExpectation *)expectation { + for (RKMappingTestEvent *event in self.events) { + if ([event.sourceKeyPath isEqualToString:expectation.sourceKeyPath] && [event.destinationKeyPath isEqualToString:expectation.destinationKeyPath]) { + return event; + } + } + + return nil; +} + +- (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(RKMappingTestExpectation *)expectation { + if (expectation.evaluationBlock) { + // Let the expectation block evaluate the match + return expectation.evaluationBlock(event.mapping, event.value); + } else if (expectation.value) { + // Use RestKit comparison magic to match values + return RKObjectIsValueEqualToValue(event.value, expectation.value); + } + + // We only wanted to know that a mapping occured between the keyPaths + return YES; +} + +- (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; + if (nil == self.destinationObject) { + self.destinationObject = [self.mapping mappableObjectForData:self.sourceObject]; + } + RKObjectMappingOperation *mappingOperation = [RKObjectMappingOperation mappingOperationFromObject:sourceObject toObject:self.destinationObject withMapping:self.mapping]; + NSError *error = nil; + mappingOperation.delegate = self; + BOOL success = [mappingOperation performMapping:&error]; + if (! success) { + [NSException raise:NSInternalInconsistencyException format:@"%@: failure when mapping from %@ to %@ with mapping %@", + [self description], self.sourceObject, self.destinationObject, self.mapping]; + } + + self.performedMapping = YES; + } +} + +- (void)verifyExpectation:(RKMappingTestExpectation *)expectation { + RKMappingTestEvent *event = [self eventMatchingKeyPathsForExpectation:expectation]; + if (event) { + // Found a matching event, check if it satisfies the expectation + if (! [self event:event satisfiesExpectation:expectation]) { + [NSException raise:NSInternalInconsistencyException format:@"%@: expectation not satisfied: %@, but instead got %@ '%@'", + [self description], expectation, [event.value class], event.value]; + } + } else { + // No match + [NSException raise:NSInternalInconsistencyException format:@"%@: expectation not satisfied: %@, but did not.", + [self description], [expectation mappingDescription]]; + } +} + +- (void)verify { + [self performMapping]; + + for (RKMappingTestExpectation *expectation in self.expectations) { + [self verifyExpectation:expectation]; + } +} + +#pragma mark - RKObjecMappingOperationDelegate + +- (void)addEvent:(RKMappingTestEvent *)event { + [self.events addObject:event]; +} + +- (void)objectMappingOperation:(RKObjectMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKObjectAttributeMapping *)mapping { + [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 new file mode 100644 index 0000000000..33a6f6e25b --- /dev/null +++ b/Code/Testing/RKMappingTestExpectation.h @@ -0,0 +1,87 @@ +// +// RKMappingTestExpectation.h +// RestKit +// +// Created by Blake Watters on 2/17/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectAttributeMapping.h" + +/** + An RKMappingTestExpectation defines an expected mapping event that should + occur during the execution of a RKMappingTest. + + @see RKMappingTest + */ +@interface RKMappingTestExpectation : NSObject + +///----------------------------------------------------------------------------- +/// @name Creating Expectations +///----------------------------------------------------------------------------- + +/** + 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. + */ ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath; + +/** + Creates and returns a new expectation specifying that a key path in a source object should be + mapped to another key path on a destination object with a given value. + + @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. + @return An expectation specifying that sourceKeyPath should be mapped to destionationKeyPath with value. + */ ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value; + +/** + Creates and returns a new expectation specifying that a key path in a source object should be + mapped to another key path on a destinaton object and that the attribute mapping and value should + evaluate to true with a given block. + + @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. + @return An expectation specifying that sourceKeyPath should be mapped to destionationKeyPath with value. + */ ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(BOOL (^)(RKObjectAttributeMapping *mapping, id value))evaluationBlock; + +///----------------------------------------------------------------------------- +/// @name Expectation Values +///----------------------------------------------------------------------------- + +/** + Returns a keyPath on the source object that a value should be mapped from. + */ +@property (nonatomic, copy, readonly) NSString *sourceKeyPath; + +/** + Returns a keyPath on the destination object that a value should be mapped to. + */ +@property (nonatomic, copy, readonly) NSString *destinationKeyPath; + +/** + Returns the expected value that should be set to the destinationKeyPath of the destination object. + */ +@property (nonatomic, strong, readonly) id value; + +/** + A block used to evaluate if the expectation has been satisfied. + */ +@property (nonatomic, copy, readonly) BOOL (^evaluationBlock)(RKObjectAttributeMapping *mapping, id value); + +/** + Returns a string summary of the expected keyPath mapping within the expectation + + @return A string describing the expected sourceKeyPath to destinationKeyPath mapping. + */ +- (NSString *)mappingDescription; + +@end diff --git a/Code/Testing/RKMappingTestExpectation.m b/Code/Testing/RKMappingTestExpectation.m new file mode 100644 index 0000000000..7613ce3959 --- /dev/null +++ b/Code/Testing/RKMappingTestExpectation.m @@ -0,0 +1,69 @@ +// +// RKMappingTestExpectation.m +// RestKit +// +// Created by Blake Watters on 2/17/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKMappingTestExpectation.h" + +@interface RKMappingTestExpectation () +@property (nonatomic, copy, readwrite) NSString *sourceKeyPath; +@property (nonatomic, copy, readwrite) NSString *destinationKeyPath; +@property (nonatomic, strong, readwrite) id value; +@property (nonatomic, copy, readwrite) BOOL (^evaluationBlock)(RKObjectAttributeMapping *mapping, id value); +@end + + +@implementation RKMappingTestExpectation + +@synthesize sourceKeyPath; +@synthesize destinationKeyPath; +@synthesize value; +@synthesize evaluationBlock; + ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath { + RKMappingTestExpectation *expectation = [self new]; + expectation.sourceKeyPath = sourceKeyPath; + expectation.destinationKeyPath = destinationKeyPath; + + return expectation; +} + ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath value:(id)value { + RKMappingTestExpectation *expectation = [self new]; + expectation.sourceKeyPath = sourceKeyPath; + expectation.destinationKeyPath = destinationKeyPath; + expectation.value = value; + + return expectation; +} + ++ (RKMappingTestExpectation *)expectationWithSourceKeyPath:(NSString *)sourceKeyPath destinationKeyPath:(NSString *)destinationKeyPath evaluationBlock:(BOOL (^)(RKObjectAttributeMapping *mapping, id value))testBlock { + RKMappingTestExpectation *expectation = [self new]; + expectation.sourceKeyPath = sourceKeyPath; + expectation.destinationKeyPath = destinationKeyPath; + expectation.evaluationBlock = testBlock; + + return expectation; +} + +- (NSString *)mappingDescription { + return [NSString stringWithFormat:@"expected sourceKeyPath '%@' to map to destinationKeyPath '%@'", + self.sourceKeyPath, self.destinationKeyPath]; +} + +- (NSString *)description { + if (self.value) { + return [NSString stringWithFormat:@"expected sourceKeyPath '%@' to map to destinationKeyPath '%@' with %@ value '%@'", + self.sourceKeyPath, self.destinationKeyPath, [self.value class], self.value]; + } else if (self.evaluationBlock) { + return [NSString stringWithFormat:@"expected sourceKeyPath '%@' to map to destinationKeyPath '%@' satisfying evaluation block", + self.sourceKeyPath, self.destinationKeyPath]; + } + + return [self mappingDescription]; +} + +@end 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 new file mode 100644 index 0000000000..b107cd5c90 --- /dev/null +++ b/Code/Testing/RKTestFactory.h @@ -0,0 +1,234 @@ +// +// RKTestFactory.h +// RKGithub +// +// Created by Blake Watters on 2/16/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#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. + + @see RKTestFactory + */ +@protocol RKTestFactoryCallbacks + +@optional + +///----------------------------------------------------------------------------- +/// @name Customizing the Factory +///----------------------------------------------------------------------------- + +/** + Application specific initialization point for the factory. + Called once per unit testing run when the factory singleton instance is initialized. RestKit + applications may override via a category. + */ ++ (void)didInitialize; + +/** + Application specific customization point for the factory. + Invoked each time the factory is asked to set up the environment. RestKit applications + leveraging the factory may override via a category. + */ ++ (void)didSetUp; + +/** + Application specific customization point for the factory. + Invoked each time the factory is tearing down the environment. RestKit applications + leveraging the factory may override via a category. + */ ++ (void)didTearDown; + +@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 + between test cases by ensuring that RestKit's important singleton objects are torn + 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 + +///----------------------------------------------------------------------------- +/// @name Configuring the Factory +///----------------------------------------------------------------------------- + +/** + 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; + +/** + Sets the base URL for the factory to a new value by constructing an RKURL + from the given string. + + @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 using the factory defined + for the name RKTestFactoryDefaultNamesClient. + + @return A new client instance. + */ ++ (id)client; + +/** + Creates and returns an RKObjectManager instance using the factory defined + for the name RKTestFactoryDefaultNamesObjectManager. + + @return A new object manager instance. + */ ++ (id)objectManager; + +/** + 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 instance. + */ ++ (id)managedObjectStore; + +///----------------------------------------------------------------------------- +/// @name Managing Test State +///----------------------------------------------------------------------------- + +/** + Sets up the RestKit testing environment. Invokes the didSetUp callback for application + specific setup. + */ ++ (void)setUp; + +/** + Tears down the RestKit testing environment by clearing singleton instances, helping to + ensure test case isolation. Invokes the didTearDown callback for application specific + cleanup. + */ ++ (void)tearDown; + +///----------------------------------------------------------------------------- +/// @name Other Tasks +///----------------------------------------------------------------------------- + +/** + Clears the contents of the cache directory by removing the directory and + recreating it. + + @see [RKDirectory cachesDirectory] + */ ++ (void)clearCacheDirectory; + +@end diff --git a/Code/Testing/RKTestFactory.m b/Code/Testing/RKTestFactory.m new file mode 100644 index 0000000000..426261aa6f --- /dev/null +++ b/Code/Testing/RKTestFactory.m @@ -0,0 +1,248 @@ +// +// RKTestFactory.m +// RKGithub +// +// Created by Blake Watters on 2/16/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestFactory.h" + +@interface RKTestFactory () + +@property (nonatomic, strong) RKURL *baseURL; +@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 + +static RKTestFactory *sharedFactory = nil; + +@implementation RKTestFactory + +@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 +{ + if (! sharedFactory) { + sharedFactory = [RKTestFactory new]; + } + + return sharedFactory; +} + +- (id)init +{ + self = [super init]; + if (self) { + self.baseURL = [RKURL URLWithString:@"http://127.0.0.1:4567"]; + self.managedObjectStoreFilename = RKTestFactoryDefaultStoreFilename; + self.factoryBlocks = [NSMutableDictionary new]; + [self defineDefaultFactories]; + } + + return self; +} + +- (void)defineFactory:(NSString *)factoryName withBlock:(id (^)())block +{ + [self.factoryBlocks setObject:[block copy] forKey:factoryName]; +} + +- (id)objectFromFactory:(NSString *)factoryName +{ + 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 + ++ (RKURL *)baseURL +{ + return [RKTestFactory sharedFactory].baseURL; +} + ++ (void)setBaseURL:(RKURL *)URL +{ + [RKTestFactory sharedFactory].baseURL = URL; +} + ++ (NSString *)baseURLString +{ + return [[[RKTestFactory sharedFactory] baseURL] absoluteString]; +} + ++ (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 +{ + RKClient *client = [self objectFromFactory:RKTestFactoryDefaultNamesClient]; + [RKClient setSharedClient:client]; + + return client; +} + ++ (id)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 *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]; + } +} + ++ (void)tearDown +{ + [RKObjectManager setSharedManager:nil]; + [RKClient setSharedClient:nil]; + [RKManagedObjectStore setDefaultObjectStore:nil]; + + if ([self respondsToSelector:@selector(didTearDown)]) { + [self didTearDown]; + } +} + ++ (void)clearCacheDirectory +{ + NSError* error = nil; + NSString* cachePath = [RKDirectory cachesDirectory]; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:cachePath error:&error]; + if (success) { + 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]); + } + } else { + RKLogError(@"Failed to clear cache path '%@': %@", cachePath, [error localizedDescription]); + } +} + +@end diff --git a/Code/Testing/RKTestFixture.h b/Code/Testing/RKTestFixture.h new file mode 100644 index 0000000000..44fc99a5bd --- /dev/null +++ b/Code/Testing/RKTestFixture.h @@ -0,0 +1,100 @@ +// +// RKTestFixture.h +// RestKit +// +// Created by Blake Watters on 2/1/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 +#if TARGET_OS_IPHONE +#import +#endif + +/** + Provides a static method API for conveniently accessing fixture data + contained within a designated NSBundle. Useful when writing unit tests that + leverage fixture data for testing parsing and object mapping operations. + */ +@interface RKTestFixture : NSObject + +/** + Returns the NSBundle object designated as the source location for unit testing fixture data. + + @return The NSBundle object designated as the source location for unit testing fixture data + or nil if none has been configured. + */ ++ (NSBundle *)fixtureBundle; + +/** + Designates the specified NSBundle object as the source location for unit testing fixture data. + + @param bundle The new fixture NSBundle object. + */ ++ (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. + + @param fixtureName The name of the fixture file. + @return A new image object for the specified fixture, or nil if the method could not initialize the image from the specified file. + */ ++ (UIImage *)imageWithContentsOfFixture:(NSString *)fixtureName; +#endif + +/** + Creates and returns a string object by reading data from the fixture identified by the specified file name using UTF-8 encoding. + + @param fixtureName The name of the fixture file. + @return A string created by reading data from the specified fixture file using the NSUTF8StringEncoding. + */ ++ (NSString *)stringWithContentsOfFixture:(NSString *)fixtureName; + +/** + Creates and returns a data object by reading every byte from the fixture identified by the specified file name. + + @param fixtureName The name of the resource file. + @return A data object by reading every byte from the fixture file. + */ ++ (NSData *)dataWithContentsOfFixture:(NSString *)fixtureName; + +/** + Returns the MIME Type for the fixture identified by the specified name. + + @param fixtureName The name of the fixture file. + @return The MIME Type for the resource file or nil if the file could not be located. + */ ++ (NSString *)MIMETypeForFixture:(NSString *)fixtureName; + +/** + Creates and returns an object representation of the data from the fixture identified by the specified file name by reading the + data as a string and parsing it using a parser appropriate for the MIME Type of the file. + + @param fixtureName The name of the resource file. + @return A new image object for the specified file, or nil if the method could not initialize the image from the specified file. + @see RKParserRegistry + */ ++ (id)parsedObjectWithContentsOfFixture:(NSString *)fixtureName; + +@end diff --git a/Code/Testing/RKTestFixture.m b/Code/Testing/RKTestFixture.m new file mode 100644 index 0000000000..3f689bac45 --- /dev/null +++ b/Code/Testing/RKTestFixture.m @@ -0,0 +1,66 @@ +// +// RKTestFixture.m +// RestKit +// +// Created by Blake Watters on 2/1/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 "RKTestFixture.h" +#import "NSBundle+RKAdditions.h" + +static NSBundle *fixtureBundle = nil; + +@implementation RKTestFixture + ++ (NSBundle *)fixtureBundle { + NSAssert(fixtureBundle != nil, @"Bundle for fixture has not been set. Use setFixtureBundle: to set it."); + return fixtureBundle; +} + ++ (void)setFixtureBundle:(NSBundle *)bundle { + NSAssert(bundle != nil, @"Bundle for fixture cannot be nil."); + [bundle retain]; + [fixtureBundle release]; + 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]; +} +#endif + ++ (NSString *)stringWithContentsOfFixture:(NSString *)fixtureName { + return [[self fixtureBundle] stringWithContentsOfResource:fixtureName withExtension:nil encoding:NSUTF8StringEncoding]; +} + ++ (NSData *)dataWithContentsOfFixture:(NSString *)fixtureName { + return [[self fixtureBundle] dataWithContentsOfResource:fixtureName withExtension:nil]; +} + ++ (NSString *)MIMETypeForFixture:(NSString *)fixtureName { + return [[self fixtureBundle] MIMETypeForResource:fixtureName withExtension:nil]; +} + ++ (id)parsedObjectWithContentsOfFixture:(NSString *)fixtureName { + return [[self fixtureBundle] parsedObjectWithContentsOfResource:fixtureName withExtension:nil]; +} + +@end diff --git a/Code/Testing/RKTestNotificationObserver.h b/Code/Testing/RKTestNotificationObserver.h new file mode 100644 index 0000000000..12389edd38 --- /dev/null +++ b/Code/Testing/RKTestNotificationObserver.h @@ -0,0 +1,90 @@ +// +// RKTestNotificationObserver.h +// RestKit +// +// Created by Jeff Arena on 8/23/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +/** + An RKTestNotificationObserver object provides support for awaiting a notification + to be posted as the result of an asynchronous operation by spinning the run loop. This + enables a straight-forward unit testing workflow by blocking execution of the test until + a notification is posted. + */ +@interface RKTestNotificationObserver : NSObject + +/** + The name of the notification the receiver is awaiting. + */ +@property (nonatomic, copy) NSString* name; + +/** + The object expected to post the notification the receiver is awaiting. + + Can be nil. + */ +@property (nonatomic, assign) id object; + +/** + The timeout interval, in seconds, to wait for the notification to be posted. + + **Default**: 3 seconds + */ +@property (nonatomic, assign) NSTimeInterval timeout; + +/** + Creates and initializes a notification obsercer object. + + @return The newly created notification observer. + */ ++ (RKTestNotificationObserver *)notificationObserver; + +/** + Instantiate a notification observer for the given notification name and object + + @param notificationName The name of the NSNotification we want to watch for + @param notificationSender The source object of the NSNotification we want to watch for + @return The newly created notification observer initialized with notificationName and notificationSender. + */ ++ (RKTestNotificationObserver *)notificationObserverForName:(NSString *)notificationName object:(id)notificationSender; + +/** + Instantiate a notification observer for the given notification name + + @param notificationName The name of the NSNotification we want to watch for + */ ++ (RKTestNotificationObserver *)notificationObserverForName:(NSString *)notificationName; + +/** + Wait for a notification matching the name and source object we are observing to be posted. + + This method will block by spinning the runloop waiting for an appropriate notification matching + our observed name and object to be posted or the timeout configured is exceeded. + */ +- (void)waitForNotification; + +/*** @name Block Helpers */ + +/** + Configures a notification observer to wait for the a notification with the given name to be posted + by the source object during execution of the block. + + @param name The name of the notification we are waiting for + @param notificationSender The object we are waiting to post the notification + @param block A block to invoke to trigger the notification activity + */ ++ (void)waitForNotificationWithName:(NSString *)name object:(id)notificationSender usingBlock:(void(^)())block; + +/** + Configures a notification observer to wait for the a notification with the given name to be posted + during execution of the block. + + @param name The name of the notification we are waiting for + @param block A block to invoke to trigger the notification activity + */ ++ (void)waitForNotificationWithName:(NSString *)name usingBlock:(void(^)())block; + +@end diff --git a/Code/Testing/RKTestNotificationObserver.m b/Code/Testing/RKTestNotificationObserver.m new file mode 100644 index 0000000000..ba20a39ebb --- /dev/null +++ b/Code/Testing/RKTestNotificationObserver.m @@ -0,0 +1,88 @@ +// +// RKTestNotificationObserver.m +// RestKit +// +// Created by Jeff Arena on 8/23/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestNotificationObserver.h" + +@interface RKTestNotificationObserver () +@property (nonatomic, assign, getter = isAwaitingNotification) BOOL awaitingNotification; +@end + +@implementation RKTestNotificationObserver + +@synthesize object; +@synthesize name; +@synthesize timeout; +@synthesize awaitingNotification; + ++ (void)waitForNotificationWithName:(NSString *)name object:(id)object usingBlock:(void(^)())block { + RKTestNotificationObserver *observer = [RKTestNotificationObserver notificationObserverForName:name object:object]; + block(); + [observer waitForNotification]; +} + ++ (void)waitForNotificationWithName:(NSString *)name usingBlock:(void(^)())block { + [self waitForNotificationWithName:name object:nil usingBlock:block]; +} + ++ (RKTestNotificationObserver *)notificationObserver { + return [[[self alloc] init] autorelease]; +} + ++ (RKTestNotificationObserver *)notificationObserverForName:(NSString *)notificationName object:(id)object { + RKTestNotificationObserver *notificationObserver = [self notificationObserver]; + notificationObserver.object = object; + notificationObserver.name = notificationName; + return notificationObserver; +} + ++ (RKTestNotificationObserver *)notificationObserverForName:(NSString *)notificationName { + return [self notificationObserverForName:notificationName object:nil]; +} + +- (id)init { + self = [super init]; + if (self) { + timeout = 5; + awaitingNotification = NO; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)waitForNotification { + NSAssert(name, @"Notification name cannot be nil"); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(processNotification:) + name:self.name + object:self.object]; + + 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; + } + } +} + +- (void)processNotification:(NSNotification*)notification { + NSAssert([name isEqualToString:notification.name], + @"Received notification (%@) differs from expected notification (%@)", + notification.name, name); + awaitingNotification = NO; +} + +@end diff --git a/Code/Testing/RKTestResponseLoader.h b/Code/Testing/RKTestResponseLoader.h new file mode 100644 index 0000000000..34a9b4baed --- /dev/null +++ b/Code/Testing/RKTestResponseLoader.h @@ -0,0 +1,108 @@ +// +// RKTestResponseLoader.h +// RestKit +// +// Created by Blake Watters on 1/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKObjectLoader.h" + +/** + An RKTestResponseLoader object provides testing support for asynchronously loading an RKRequest or + RKObjectLoader object while blocking the execution of the current thread by spinning the run loop. + This enables a straight-forward unit testing workflow for asynchronous network operations. + + RKTestResponseLoader instances are designed to act as as the delegate for an RKObjectLoader or RKRequest + object under test. Once assigned as the delegate to a request and the request has been sent, + waitForResponse: is invoked to block execution until the response is loaded. + */ +@interface RKTestResponseLoader : NSObject + +/** + The RKResponse object loaded from the RKRequest or RKObjectLoader the receiver is acting as the delegate for. + **/ +@property (nonatomic, retain, readonly) RKResponse *response; + +/** + The collection of objects loaded from the RKObjectLoader the receiver is acting as the delegate for. + */ +@property (nonatomic, retain, readonly) NSArray *objects; + +/** + A Boolean value that indicates whether a response was loaded successfully. + + @return YES if a response was loaded successfully. + */ +@property (nonatomic, readonly, getter = wasSuccessful) BOOL successful; + +/** + A Boolean value that indicates whether the RKRequest or RKObjectLoader the receiver is acting as the delegate for was cancelled. + + @return YES if the request was cancelled + */ +@property (nonatomic, readonly, getter = wasCancelled) BOOL cancelled; + +/** + A Boolean value that indicates if an unexpected response was loaded. + + @return YES if the request loaded an unknown response. + @see [RKObjectLoaderDelegate objectLoaderDidLoadUnexpectedResponse:] + */ +@property (nonatomic, readonly, getter = loadedUnexpectedResponse) BOOL unexpectedResponse; + +/** + An NSError value that was loaded from the RKRequest or RKObjectLoader the receiver is acting as the delegate for. + + @see [RKRequestDelegate request:didFailLoadWithError:] + @see [RKObjectLoaderDelegate objectLoader:didFailWithError:] + */ +@property (nonatomic, copy, readonly) NSError *error; + +/** + The timeout interval, in seconds, to wait for a response to load. + + The default value is 4 seconds. + + @see [RKTestResponseLoader waitForResponse] + */ +@property (nonatomic, assign) NSTimeInterval timeout; + +/** + Creates and returns a test response loader object. + + @return A new response loader object. + */ ++ (id)responseLoader; + +/** + Waits for an asynchronous RKRequest or RKObjectLoader network operation to load a response + by spinning the current run loop to block the current thread of execution. + + The wait operation is guarded by a timeout + */ +- (void)waitForResponse; + +/** + Returns the localized description error message for the error. + + TODO: Why not just move this to NSError+RKAdditions? + + @return The localized description of the error or nil. + */ +- (NSString *)errorMessage; + +@end diff --git a/Code/Testing/RKTestResponseLoader.m b/Code/Testing/RKTestResponseLoader.m new file mode 100644 index 0000000000..a3e47cda41 --- /dev/null +++ b/Code/Testing/RKTestResponseLoader.m @@ -0,0 +1,171 @@ +// +// RKTestResponseLoader.m +// RestKit +// +// Created by Blake Watters on 1/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestResponseLoader.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitTesting + +NSString * const RKTestResponseLoaderTimeoutException = @"RKTestResponseLoaderTimeoutException"; + +@interface RKTestResponseLoader () + +@property (nonatomic, assign, getter = isAwaitingResponse) BOOL awaitingResponse; +@property (nonatomic, retain, readwrite) RKResponse *response; +@property (nonatomic, copy, readwrite) NSError *error; +@property (nonatomic, retain, readwrite) NSArray *objects; + +@end + +@implementation RKTestResponseLoader + +@synthesize response; +@synthesize objects; +@synthesize error; +@synthesize successful; +@synthesize timeout; +@synthesize cancelled; +@synthesize unexpectedResponse; +@synthesize awaitingResponse; + ++ (RKTestResponseLoader *)responseLoader { + return [[[self alloc] init] autorelease]; +} + +- (id)init { + self = [super init]; + if (self) { + timeout = 4; + awaitingResponse = NO; + } + + return self; +} + +- (void)dealloc { + [response release]; + response = nil; + [error release]; + error = nil; + [objects release]; + objects = nil; + + [super dealloc]; +} + +- (void)waitForResponse { + 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; + } + } +} + +- (void)loadError:(NSError *)theError { + awaitingResponse = NO; + successful = NO; + self.error = theError; +} + +- (NSString *)errorMessage { + if (self.error) { + return [[self.error userInfo] valueForKey:NSLocalizedDescriptionKey]; + } + + return nil; +} + +- (void)request:(RKRequest *)request didReceiveResponse:(RKResponse *)response { + // Implemented for expectations +} + +- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)aResponse { + self.response = aResponse; + + // If request is an Object Loader, then objectLoader:didLoadObjects: + // will be sent after didLoadResponse: + if (NO == [request isKindOfClass:[RKObjectLoader class]]) { + awaitingResponse = NO; + successful = YES; + } +} + +- (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)anError { + // If request is an Object Loader, then objectLoader:didFailWithError: + // will be sent after didFailLoadWithError: + if (NO == [request isKindOfClass:[RKObjectLoader class]]) { + [self loadError:anError]; + } + + // Ensure we get no further delegate messages + [request cancel]; +} + +- (void)requestDidCancelLoad:(RKRequest *)request { + awaitingResponse = NO; + successful = NO; + cancelled = YES; +} + +- (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; +} + +- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)theError { + [self loadError:theError]; +} + +- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader { + RKLogDebug(@"%@ Loaded unexpected response for: %@", self, objectLoader); + successful = NO; + awaitingResponse = NO; + unexpectedResponse = YES; +} + +- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { + // Implemented for expectations +} + +#pragma mark - OAuth delegates + +- (void)OAuthClient:(RKOAuthClient *)client didAcquireAccessToken:(NSString *)token { + awaitingResponse = NO; + successful = YES; +} + + +- (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidGrantError:(NSError *)error { + awaitingResponse = NO; + successful = NO; +} + +@end diff --git a/Code/Testing/Testing.h b/Code/Testing/Testing.h new file mode 100644 index 0000000000..2881f6f3d1 --- /dev/null +++ b/Code/Testing/Testing.h @@ -0,0 +1,13 @@ +// +// Testing.h +// RestKit +// +// Created by Blake Watters on 2/1/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestFixture.h" +#import "RKTestNotificationObserver.h" +#import "RKTestResponseLoader.h" +#import "RKTestFactory.h" +#import "RKMappingTest.h" diff --git a/Code/UI/RKAbstractTableController.h b/Code/UI/RKAbstractTableController.h new file mode 100755 index 0000000000..d6faea9f57 --- /dev/null +++ b/Code/UI/RKAbstractTableController.h @@ -0,0 +1,444 @@ +// +// RKAbstractTableController.h +// RestKit +// +// Created by Jeff Arena on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if TARGET_OS_IPHONE + +#import +#import "RKTableViewCellMappings.h" +#import "RKTableItem.h" +#import "RKObjectManager.h" +#import "RKObjectMapping.h" +#import "RKObjectLoader.h" + +///----------------------------------------------------------------------------- +/// @name Constants +///----------------------------------------------------------------------------- + +/** + 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 controller 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 controller 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 controller has transitioned from an online to an offline state. + */ +extern NSString * const RKTableControllerDidBecomeOffline; + +@protocol RKAbstractTableControllerDelegate; + +/** + @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; + +/** + 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, assign) UITableViewRowAnimation defaultRowAnimation; + +@property (nonatomic, assign) BOOL pullToRefreshEnabled; +@property (nonatomic, assign) BOOL canEditRows; +@property (nonatomic, assign) BOOL canMoveRows; +@property (nonatomic, assign) BOOL autoResizesForKeyboard; + +///----------------------------------------------------------------------------- +/// @name Instantiation +///----------------------------------------------------------------------------- + ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController; + ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController; + +- (id)initWithTableView:(UITableView *)tableView + viewController:(UIViewController *)viewController; + +///----------------------------------------------------------------------------- +/// @name Object to Table View Cell Mappings +///----------------------------------------------------------------------------- + +@property (nonatomic, retain) RKTableViewCellMappings *cellMappings; + +- (void)mapObjectsWithClass:(Class)objectClass toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; +- (void)mapObjectsWithClassName:(NSString *)objectClassName toTableCellsWithMapping:(RKTableViewCellMapping *)cellMapping; +- (id)objectForRowAtIndexPath:(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; +- (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping; +- (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping; +- (void)removeAllHeaderRows; +- (void)removeAllFooterRows; + +///----------------------------------------------------------------------------- +/// @name RESTful Table Loading +///----------------------------------------------------------------------------- + +/** + 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. + Online/offline state is also determined by watching for reachability + notifications generated from the object manager. + + **Default**: The shared manager instance `[RKObjectManager sharedManager]` + */ +@property (nonatomic, assign) RKObjectManager *objectManager; +@property (nonatomic, assign) BOOL autoRefreshFromNetwork; +@property (nonatomic, assign) NSTimeInterval autoRefreshRate; + +- (void)loadTableWithObjectLoader:(RKObjectLoader *)objectLoader; +- (void)cancelLoad; +- (BOOL)isAutoRefreshNeeded; + +///----------------------------------------------------------------------------- +/// @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; + +/** + 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. + */ +@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. + */ +@property (nonatomic, retain) UIImage *imageForError; + +/** + An image to overlay onto the table with when the user does + not have connectivity to the Internet. + + @see RKReachabilityObserver + */ +@property (nonatomic, retain) UIImage *imageForOffline; + +/** + A UIView to add to the table overlay during loading. It + will be positioned directly in the center of the table view. + + The loading view is always presented non-modally. + */ +@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 + applied to the table view during display of the loading view and + state overlay images (offline/error/empty). By default, the overlay + view will be auto-sized to cover the entire table. This can result in + an inaccessible table UI if you have embedded controls within the header + or footer views of your table. You can adjust the frame of the overlay + precisely by configuring the overlayFrame property. + */ +@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 + and prevent any interaction with the table. + + **Default**: YES + */ +@property (nonatomic, assign) BOOL showsOverlayImagesModally; + +// Default NO +@property (nonatomic, assign) BOOL variableHeightRows; +@property (nonatomic, assign) BOOL showsHeaderRowsWhenEmpty; +@property (nonatomic, assign) BOOL showsFooterRowsWhenEmpty; +@property (nonatomic, retain) RKTableItem *emptyItem; + +///----------------------------------------------------------------------------- +/// @name Managing Sections +///----------------------------------------------------------------------------- + +/** + The number of sections in the table. + */ +@property (nonatomic, readonly) NSUInteger sectionCount; + +/** + The number of rows across all sections in the model. + */ +@property (nonatomic, readonly) NSUInteger rowCount; + +/** + Returns the number of rows in the section at the given index. + + @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. + */ +- (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, readonly) id swipeObject; +@property (nonatomic, readonly) BOOL animatingCellSwipe; +@property (nonatomic, readonly) UISwipeGestureRecognizerDirection swipeDirection; + +- (void)addSwipeViewTo:(UITableViewCell *)cell withObject:(id)object direction:(UISwipeGestureRecognizerDirection)direction; +- (void)removeSwipeView:(BOOL)animated; + +@end + +@protocol RKAbstractTableControllerDelegate + +@optional + +// Network +- (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 + */ +- (void)tableControllerDidFinishLoad:(RKAbstractTableController *)tableController; +- (void)tableController:(RKAbstractTableController *)tableController didFailLoadWithError:(NSError *)error; +- (void)tableControllerDidCancelLoad:(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; + +/** + 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 controller has transitioned from online to offline + */ +- (void)tableControllerDidBecomeOffline:(RKAbstractTableController *)tableController; + +// Objects +- (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; + +// Editing +- (void)tableController:(RKAbstractTableController *)tableController willBeginEditing:(id)object atIndexPath:(NSIndexPath *)indexPath; +- (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; + +// 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; + +@end + +#endif // TARGET_OS_IPHONE diff --git a/Code/UI/RKAbstractTableController.m b/Code/UI/RKAbstractTableController.m new file mode 100755 index 0000000000..9351bbfc64 --- /dev/null +++ b/Code/UI/RKAbstractTableController.m @@ -0,0 +1,1347 @@ +// +// RKAbstractTableController.m +// RestKit +// +// Created by Jeff Arena on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKAbstractTableController.h" +#import "RKAbstractTableController_Internals.h" +#import "RKObjectMappingOperation.h" +#import "RKLog.h" +#import "RKErrors.h" +#import "RKReachabilityObserver.h" +#import "UIView+FindFirstResponder.h" +#import "RKRefreshGestureRecognizer.h" +#import "RKTableSection.h" + +// Define logging component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitUI + +/** + Bounce pixels define how many pixels the cell swipe view is + moved during the bounce animation + */ +#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"; + +static NSString * lastUpdatedDateDictionaryKey = @"lastUpdatedDateDictionaryKey"; + +@implementation RKAbstractTableController + +@synthesize delegate = _delegate; +@synthesize viewController = _viewController; +@synthesize tableView = _tableView; +@synthesize defaultRowAnimation = _defaultRowAnimation; + +@synthesize objectLoader = _objectLoader; +@synthesize objectManager = _objectManager; +@synthesize cellMappings = _cellMappings; +@synthesize autoRefreshFromNetwork = _autoRefreshFromNetwork; +@synthesize autoRefreshRate = _autoRefreshRate; + +@synthesize state = _state; +@synthesize error = _error; + +@synthesize imageForEmpty = _imageForEmpty; +@synthesize imageForError = _imageForError; +@synthesize imageForOffline = _imageForOffline; +@synthesize loadingView = _loadingView; + +@synthesize variableHeightRows = _variableHeightRows; +@synthesize showsHeaderRowsWhenEmpty = _showsHeaderRowsWhenEmpty; +@synthesize showsFooterRowsWhenEmpty = _showsFooterRowsWhenEmpty; +@synthesize pullToRefreshEnabled = _pullToRefreshEnabled; +@synthesize headerItems = _headerItems; +@synthesize footerItems = _footerItems; +@synthesize canEditRows = _canEditRows; +@synthesize canMoveRows = _canMoveRows; +@synthesize autoResizesForKeyboard = _autoResizesForKeyboard; +@synthesize emptyItem = _emptyItem; + +@synthesize cellSwipeViewsEnabled = _cellSwipeViewsEnabled; +@synthesize cellSwipeView = _cellSwipeView; +@synthesize swipeCell = _swipeCell; +@synthesize animatingCellSwipe = _animatingCellSwipe; +@synthesize swipeDirection = _swipeDirection; +@synthesize swipeObject = _swipeObject; + +@synthesize showsOverlayImagesModally = _modalOverlay; +@synthesize overlayFrame = _overlayFrame; +@synthesize tableOverlayView = _tableOverlayView; +@synthesize stateOverlayImageView = _stateOverlayImageView; +@synthesize cache = _cache; +@synthesize pullToRefreshHeaderView = _pullToRefreshHeaderView; + +#pragma mark - Instantiation + ++ (id)tableControllerWithTableView:(UITableView *)tableView + forViewController:(UIViewController *)viewController { + return [[[self alloc] initWithTableView:tableView viewController:viewController] autorelease]; +} + ++ (id)tableControllerForTableViewController:(UITableViewController *)tableViewController { + return [self tableControllerWithTableView:tableViewController.tableView + forViewController:tableViewController]; +} + +- (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; + _viewController = theViewController; // Assign directly to avoid side-effect of overloaded accessor method + self.variableHeightRows = NO; + self.defaultRowAnimation = UITableViewRowAnimationFade; + self.overlayFrame = CGRectZero; + self.showsOverlayImagesModally = YES; + } + + return self; +} + +- (id)init { + self = [super init]; + if (self) { + if ([self isMemberOfClass:[RKAbstractTableController class]]) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"%@ is abstract. Instantiate one its subclasses instead.", + NSStringFromClass([self class])] + userInfo:nil]; + } + + self.state = RKTableControllerStateNotYetLoaded; + self.objectManager = [RKObjectManager sharedManager]; + _cellMappings = [RKTableViewCellMappings new]; + + _headerItems = [NSMutableArray new]; + _footerItems = [NSMutableArray new]; + _showsHeaderRowsWhenEmpty = YES; + _showsFooterRowsWhenEmpty = YES; + + // Setup autoRefreshRate to (effectively) never + _autoRefreshFromNetwork = NO; + _autoRefreshRate = NSTimeIntervalSince1970; + + // Setup key-value observing + [self addObserver:self + forKeyPath:@"state" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; + [self addObserver:self + forKeyPath:@"error" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; + } + return self; +} + +- (void)dealloc { + // Disconnect from the tableView + if (_tableView.delegate == self) _tableView.delegate = nil; + if (_tableView.dataSource == self) _tableView.dataSource = nil; + _tableView = nil; + + // Remove overlay and pull-to-refresh subviews + [_stateOverlayImageView removeFromSuperview]; + [_stateOverlayImageView release]; + _stateOverlayImageView = nil; + [_tableOverlayView removeFromSuperview]; + [_tableOverlayView release]; + _tableOverlayView = nil; + + // Remove observers + [self removeObserver:self forKeyPath:@"state"]; + [self removeObserver:self forKeyPath:@"error"]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // TODO: WTF? Get UI crashes when enabled... +// [_objectManager.requestQueue abortRequestsWithDelegate:self]; + _objectLoader.delegate = nil; + [_objectLoader release]; + _objectLoader = nil; + + [_cellMappings release]; + [_headerItems release]; + [_footerItems release]; + [_cellSwipeView release]; + [_swipeCell release]; + [_swipeObject release]; + [_emptyItem release]; + [super dealloc]; +} + +- (void)setTableView:(UITableView *)tableView { + NSAssert(tableView, @"Cannot assign a nil tableView to the model"); + _tableView = tableView; + _tableView.delegate = self; + _tableView.dataSource = self; +} + +- (void)setViewController:(UIViewController *)viewController { + if ([viewController isKindOfClass:[UITableViewController class]]) { + self.tableView = [(UITableViewController*)viewController tableView]; + } +} + +- (void)setObjectManager:(RKObjectManager *)objectManager { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + // Remove observers + if (_objectManager) { + [notificationCenter removeObserver:self + name:RKObjectManagerDidBecomeOfflineNotification + object:_objectManager]; + [notificationCenter removeObserver:self + name:RKObjectManagerDidBecomeOnlineNotification + object:_objectManager]; + } + + _objectManager = objectManager; + + 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; + } + } + } +} + +- (void)setAutoResizesForKeyboard:(BOOL)autoResizesForKeyboard { + if (_autoResizesForKeyboard != autoResizesForKeyboard) { + _autoResizesForKeyboard = autoResizesForKeyboard; + if (_autoResizesForKeyboard) { + // Register for Keyboard notifications + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(resizeTableViewForKeyboard:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(resizeTableViewForKeyboard:) + name:UIKeyboardWillHideNotification + object:nil]; + } else { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } + } +} + +- (void)setAutoRefreshFromNetwork:(BOOL)autoRefreshFromNetwork { + if (_autoRefreshFromNetwork != autoRefreshFromNetwork) { + _autoRefreshFromNetwork = autoRefreshFromNetwork; + if (_autoRefreshFromNetwork) { + NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] + stringByAppendingPathComponent:@"RKAbstractTableControllerCache"]; + _cache = [[RKCache alloc] initWithPath:cachePath subDirectories:nil]; + } else { + if (_cache) { + [_cache invalidateAll]; + [_cache release]; + _cache = nil; + } + } + } +} + +- (void)setLoading:(BOOL)loading { + if (loading) { + self.state |= RKTableControllerStateLoading; + } else { + self.state &= ~RKTableControllerStateLoading; + } +} + +// 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; + } +} + +- (void)setEmpty:(BOOL)empty { + if (empty) { + self.state |= RKTableControllerStateEmpty; + } else { + self.state &= ~RKTableControllerStateEmpty; + } +} + +- (void)setOffline:(BOOL)offline { + if (offline) { + self.state |= RKTableControllerStateOffline; + } else { + self.state &= ~RKTableControllerStateOffline; + } +} + +- (void)setErrorState:(BOOL)error { + if (error) { + self.state |= RKTableControllerStateError; + } else { + self.state &= ~RKTableControllerStateError; + } +} + +- (void)objectManagerConnectivityDidChange:(NSNotification *)notification { + RKLogTrace(@"%@ received network status change notification: %@", self, [notification name]); + [self setOffline:!self.objectManager.isOnline]; +} + +#pragma mark - Abstract Methods + +- (BOOL)isConsideredEmpty { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (NSUInteger)sectionCount { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (NSUInteger)rowCount { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (NSIndexPath *)indexPathForObject:(id)object { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)index { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (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 { + // 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 { + [self mapObjectsWithClass:NSClassFromString(objectClassName) toTableCellsWithMapping:cellMapping]; +} + +- (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]; +} + +- (UITableViewCell *)cellForObject:(id)object { + NSIndexPath *indexPath = [self indexPathForObject:object]; + return indexPath ? [self cellForObjectAtIndexPath:indexPath] : nil; +} + +#pragma mark - Header and Footer Rows + +- (void)addHeaderRowForItem:(RKTableItem *)tableItem { + [_headerItems addObject:tableItem]; +} + +- (void)addFooterRowForItem:(RKTableItem *)tableItem { + [_footerItems addObject:tableItem]; +} + +- (void)addHeaderRowWithMapping:(RKTableViewCellMapping *)cellMapping { + RKTableItem *tableItem = [RKTableItem tableItem]; + tableItem.cellMapping = cellMapping; + [self addHeaderRowForItem:tableItem]; +} + +- (void)addFooterRowWithMapping:(RKTableViewCellMapping *)cellMapping { + RKTableItem *tableItem = [RKTableItem tableItem]; + tableItem.cellMapping = cellMapping; + [self addFooterRowForItem:tableItem]; +} + +- (void)removeAllHeaderRows { + [_headerItems removeAllObjects]; +} + +- (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 { + 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]; + + // NOTE: Handle deselection first as the onSelectCell processing may result in the tableView + // being reloaded and our instances invalidated + if (cellMapping.deselectsRowOnSelection) { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; + } + + if (cellMapping.onSelectCell) { + cellMapping.onSelectCell(); + } + + if (cellMapping.onSelectCellForObjectAtIndexPath) { + 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]; + 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!!! + SEL willDisplaySelector = @selector(willDisplayInTableViewCell:); + if ([mappableObject respondsToSelector:willDisplaySelector]) { + [mappableObject performSelector:willDisplaySelector withObject:cell]; + } + + // Handle hiding header/footer rows when empty + if ([self isEmpty]) { + if (! self.showsHeaderRowsWhenEmpty && [_headerItems containsObject:mappableObject]) { + cell.hidden = YES; + } + + if (! self.showsFooterRowsWhenEmpty && [_footerItems containsObject:mappableObject]) { + cell.hidden = YES; + } + } else { + if (self.emptyItem && [self.emptyItem isEqual:mappableObject]) { + cell.hidden = YES; + } + } +} + +// Variable height support + +- (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (self.variableHeightRows) { + RKTableViewCellMapping *cellMapping = [self cellMappingForObjectAtIndexPath:indexPath]; + + if (cellMapping.heightOfCellForObjectAtIndexPath) { + id object = [self objectForRowAtIndexPath:indexPath]; + CGFloat height = cellMapping.heightOfCellForObjectAtIndexPath(object, indexPath); + RKLogTrace(@"Variable row height configured for tableView. Height via block invocation for row at indexPath '%@' = %f", indexPath, cellMapping.rowHeight); + return height; + } else { + RKLogTrace(@"Variable row height configured for tableView. Height for row at indexPath '%@' = %f", indexPath, cellMapping.rowHeight); + return cellMapping.rowHeight; + } + } + + RKLogTrace(@"Uniform row height configured for tableView. Table view row height = %f", self.tableView.rowHeight); + return self.tableView.rowHeight; +} + +- (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]; + id object = [self objectForRowAtIndexPath:indexPath]; + cellMapping.onTapAccessoryButtonForObjectAtIndexPath(cell, object, 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]; + id object = [self objectForRowAtIndexPath:indexPath]; + return cellMapping.titleForDeleteButtonForObjectAtIndexPath(cell, object, indexPath); + } + return NSLocalizedString(@"Delete", nil); +} + +- (UITableViewCellEditingStyle)tableView:(UITableView *)theTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { + if (_canEditRows) { + 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]; + return cellMapping.editingStyleForObjectAtIndexPath(cell, object, indexPath); + } + return UITableViewCellEditingStyleDelete; + } + return UITableViewCellEditingStyleNone; +} + +- (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 { + 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 { + if (_canMoveRows) { + 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]; + id object = [self objectForRowAtIndexPath:sourceIndexPath]; + return cellMapping.targetIndexPathForMove(cell, object, sourceIndexPath, proposedDestinationIndexPath); + } + } + return proposedDestinationIndexPath; +} + +- (NSIndexPath *)tableView:(UITableView *)theTableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [self removeSwipeView:YES]; + return indexPath; +} + +#pragma mark - Network Table Loading + +- (void)cancelLoad { + [self.objectLoader cancel]; +} + +- (NSDate *)lastUpdatedDate { + if (! self.objectLoader) { + return nil; + } + + if (_autoRefreshFromNetwork) { + NSAssert(_cache, @"Found a nil cache when trying to read our last loaded time"); + 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]; + if (absoluteURLString && lastUpdatedTimeIntervalSince1970) { + return [NSDate dateWithTimeIntervalSince1970:[lastUpdatedTimeIntervalSince1970 doubleValue]]; + } + } + } + return nil; +} + +- (BOOL)isAutoRefreshNeeded { + BOOL isAutoRefreshNeeded = NO; + if (_autoRefreshFromNetwork) { + isAutoRefreshNeeded = YES; + NSDate *lastUpdatedDate = [self lastUpdatedDate]; + RKLogTrace(@"Last updated: %@", lastUpdatedDate); + if (lastUpdatedDate) { + RKLogTrace(@"-timeIntervalSinceNow=%f, autoRefreshRate=%f", + -[lastUpdatedDate timeIntervalSinceNow], _autoRefreshRate); + isAutoRefreshNeeded = (-[lastUpdatedDate timeIntervalSinceNow] > _autoRefreshRate); + } + } + return isAutoRefreshNeeded; +} + +#pragma mark - RKRequestDelegate & RKObjectLoaderDelegate methods + +- (void)requestDidStartLoad:(RKRequest *)request { + RKLogTrace(@"tableController %@ started loading.", self); + [self didStartLoad]; +} + +- (void)requestDidCancelLoad:(RKRequest *)request { + RKLogTrace(@"tableController %@ cancelled loading.", self); + self.loading = NO; + + if ([self.delegate respondsToSelector:@selector(tableControllerDidCancelLoad:)]) { + [self.delegate tableControllerDidCancelLoad:self]; + } +} + +- (void)requestDidTimeout:(RKRequest *)request { + RKLogTrace(@"tableController %@ timed out while loading.", self); + self.loading = NO; +} + +- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { + RKLogTrace(@"tableController %@ finished loading.", self); + + // 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]; + if (lastUpdatedDates) { + [_cache invalidateEntry:lastUpdatedDateDictionaryKey]; + } else { + lastUpdatedDates = [[NSMutableDictionary alloc] init]; + } + NSNumber *timeIntervalSince1970 = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; + RKLogTrace(@"Setting timeIntervalSince1970=%@ for URL %@", timeIntervalSince1970, [request.URL absoluteString]); + [lastUpdatedDates setObject:timeIntervalSince1970 + forKey:[request.URL absoluteString]]; + [_cache writeDictionary:lastUpdatedDates withCacheKey:lastUpdatedDateDictionaryKey]; + [lastUpdatedDates release]; + } +} + +- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error { + RKLogError(@"tableController %@ failed network load with error: %@", self, error); + [self didFailLoadWithError:error]; +} + +- (void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader { + if ([self.delegate respondsToSelector:@selector(tableController:didLoadTableWithObjectLoader:)]) { + [self.delegate tableController:self didLoadTableWithObjectLoader:objectLoader]; + } + + [objectLoader reset]; + [self didFinishLoad]; +} + +- (void)didStartLoad { + self.loading = YES; +} + +- (void)didFailLoadWithError:(NSError *)error { + self.error = error; + [self didFinishLoad]; +} + +- (void)didFinishLoad { + 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]; + } +} + +#pragma mark - Table Overlay Views + +- (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; +} + +- (UIImage *)overlayImage { + return _stateOverlayImageView.image; +} + +// Adds an overlay view above the table +- (void)addToOverlayView:(UIView *)view modally:(BOOL)modally { + if (! _tableOverlayView) { + CGRect overlayFrame = CGRectIsEmpty(self.overlayFrame) ? self.tableView.frame : self.overlayFrame; + _tableOverlayView = [[UIView alloc] initWithFrame:overlayFrame]; + _tableOverlayView.autoresizesSubviews = YES; + _tableOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin; + NSInteger tableIndex = [_tableView.superview.subviews indexOfObject:_tableView]; + if (tableIndex != NSNotFound) { + [_tableView.superview addSubview:_tableOverlayView]; + } + } + + // When modal, we enable user interaction to catch & discard events on the overlay and its subviews + _tableOverlayView.userInteractionEnabled = modally; + view.userInteractionEnabled = modally; + + if (CGRectIsEmpty(view.frame)) { + view.frame = _tableOverlayView.bounds; + + // Center it in the overlay + view.center = _tableOverlayView.center; + } + + [_tableOverlayView addSubview:view]; +} + +- (void)resetOverlayView { + if (_stateOverlayImageView && _stateOverlayImageView.image == nil) { + [_stateOverlayImageView removeFromSuperview]; + } + if (_tableOverlayView && _tableOverlayView.subviews.count == 0) { + [_tableOverlayView removeFromSuperview]; + [_tableOverlayView release]; + _tableOverlayView = nil; + } +} + +- (void)addSubviewOverTableView:(UIView *)view { + NSInteger tableIndex = [_tableView.superview.subviews + indexOfObject:_tableView]; + if (NSNotFound != tableIndex) { + [_tableView.superview addSubview:view]; + } +} + +- (BOOL)removeImageFromOverlay:(UIImage *)image { + if (image && _stateOverlayImageView.image == image) { + _stateOverlayImageView.image = nil; + return YES; + } + return NO; +} + +- (void)showImageInOverlay:(UIImage *)image { + NSAssert(self.tableView, @"Cannot add an overlay image to a nil tableView"); + if (! _stateOverlayImageView) { + _stateOverlayImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + _stateOverlayImageView.opaque = YES; + _stateOverlayImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin; + _stateOverlayImageView.contentMode = UIViewContentModeCenter; + } + _stateOverlayImageView.image = image; + [self addToOverlayView:_stateOverlayImageView modally:self.showsOverlayImagesModally]; +} + +- (void)removeImageOverlay { + _stateOverlayImageView.image = nil; + [_stateOverlayImageView removeFromSuperview]; + [self resetOverlayView]; +} + +- (void)setImageForEmpty:(UIImage *)imageForEmpty { + [imageForEmpty retain]; + BOOL imageRemoved = [self removeImageFromOverlay:_imageForEmpty]; + [_imageForEmpty release]; + _imageForEmpty = imageForEmpty; + if (imageRemoved) [self showImageInOverlay:_imageForEmpty]; +} + +- (void)setImageForError:(UIImage *)imageForError { + [imageForError retain]; + BOOL imageRemoved = [self removeImageFromOverlay:_imageForError]; + [_imageForError release]; + _imageForError = imageForError; + if (imageRemoved) [self showImageInOverlay:_imageForError]; +} + +- (void)setImageForOffline:(UIImage *)imageForOffline { + [imageForOffline retain]; + BOOL imageRemoved = [self removeImageFromOverlay:_imageForOffline]; + [_imageForOffline release]; + _imageForOffline = imageForOffline; + if (imageRemoved) [self showImageInOverlay:_imageForOffline]; +} + +- (void)setLoadingView:(UIView *)loadingView { + [loadingView retain]; + BOOL viewRemoved = (_loadingView.superview != nil); + [_loadingView removeFromSuperview]; + [self resetOverlayView]; + [_loadingView release]; + _loadingView = loadingView; + if (viewRemoved) [self addToOverlayView:_loadingView modally:NO]; +} + +#pragma mark - KVO & Table States + +- (BOOL)isLoading { + return (self.state & RKTableControllerStateLoading) != 0; +} + +- (BOOL)isLoaded { + return (self.state & RKTableControllerStateNotYetLoaded) == 0; +// return self.state != RKTableControllerStateNotYetLoaded; +} + +- (BOOL)isOffline { + return (self.state & RKTableControllerStateOffline) != 0; +} +- (BOOL)isOnline { + return ![self isOffline]; +} + +- (BOOL)isError { + return (self.state & RKTableControllerStateError) != 0; +} + +- (BOOL)isEmpty { + return (self.state & RKTableControllerStateEmpty) != 0; +} + +- (void)isLoadingDidChange { + if ([self isLoading]) { + if ([self.delegate respondsToSelector:@selector(tableControllerDidStartLoad:)]) { + [self.delegate tableControllerDidStartLoad:self]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidStartLoadNotification object:self]; + + if (self.loadingView) { + [self addToOverlayView:self.loadingView modally:NO]; + } + } else { + if ([self.delegate respondsToSelector:@selector(tableControllerDidFinishLoad:)]) { + [self.delegate tableControllerDidFinishLoad:self]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidFinishLoadNotification object:self]; + + if (self.loadingView) { + [self.loadingView removeFromSuperview]; + [self resetOverlayView]; + } + + [self resetPullToRefreshRecognizer]; + } + + // We don't want any image overlays applied until loading is finished + _stateOverlayImageView.hidden = [self isLoading]; +} + +- (void)isLoadedDidChange { + if ([self isLoaded]) { + RKLogDebug(@"%@: is now loaded.", self); + } else { + RKLogDebug(@"%@: is NOT loaded.", self); + } +} + +- (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]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadErrorNotification object:self userInfo:userInfo]; + } +} + +- (void)isEmptyDidChange { + if ([self isEmpty]) { + if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeEmpty:)]) { + [self.delegate tableControllerDidBecomeEmpty:self]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidLoadEmptyNotification object:self]; + } +} + +- (void)isOnlineDidChange { + if ([self isOnline]) { + // We just transitioned to online + if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOnline:)]) { + [self.delegate tableControllerDidBecomeOnline:self]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOnline object:self]; + } else { + // We just transitioned to offline + if ([self.delegate respondsToSelector:@selector(tableControllerDidBecomeOffline:)]) { + [self.delegate tableControllerDidBecomeOffline:self]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:RKTableControllerDidBecomeOffline object:self]; + } +} + +- (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 { + if ([keyPath isEqualToString:@"state"]) { + [self updateTableViewForStateChange:change]; + } else if ([keyPath isEqualToString:@"error"]) { + [self setErrorState:(self.error != nil)]; + } +} + +#pragma mark - Pull to Refresh + +- (RKRefreshGestureRecognizer *)pullToRefreshGestureRecognizer { + RKRefreshGestureRecognizer *refreshRecognizer = nil; + for (RKRefreshGestureRecognizer *recognizer in self.tableView.gestureRecognizers) { + if ([recognizer isKindOfClass:[RKRefreshGestureRecognizer class]]) { + refreshRecognizer = recognizer; + break; + } + } + return refreshRecognizer; +} + +- (void)setPullToRefreshEnabled:(BOOL)pullToRefreshEnabled { + RKRefreshGestureRecognizer *recognizer = nil; + if (pullToRefreshEnabled) { + recognizer = [[[RKRefreshGestureRecognizer alloc] initWithTarget:self action:@selector(pullToRefreshStateChanged:)] autorelease]; + [self.tableView addGestureRecognizer:recognizer]; + } + else { + recognizer = [self pullToRefreshGestureRecognizer]; + if (recognizer) + [self.tableView removeGestureRecognizer:recognizer]; + } + _pullToRefreshEnabled = pullToRefreshEnabled; +} + +- (void)pullToRefreshStateChanged:(UIGestureRecognizer *)gesture { + if (gesture.state == UIGestureRecognizerStateRecognized) { + if ([self pullToRefreshDataSourceIsLoading:gesture]) + return; + RKLogDebug(@"%@: pull to refresh triggered from gesture: %@", self, gesture); + if (self.objectLoader) { + [self.objectLoader reset]; + [self.objectLoader send]; + } + } +} + +- (void)resetPullToRefreshRecognizer { + 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]; +} + +- (NSDate *)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer *)gesture { + NSDate *dataSourceLastUpdated = [self lastUpdatedDate]; + return dataSourceLastUpdated ? dataSourceLastUpdated : [NSDate date]; +} + +#pragma mark - Cell Swipe Menu Methods + +- (void)setupSwipeGestureRecognizers { + // Setup a right swipe gesture recognizer + 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:)]; + leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; + [self.tableView addGestureRecognizer:leftSwipeGestureRecognizer]; + [leftSwipeGestureRecognizer release]; +} + +- (void)removeSwipeGestureRecognizers { + for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) { + if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]]) { + [self.tableView removeGestureRecognizer:recognizer]; + } + } +} + +- (void)setCanEditRows:(BOOL)canEditRows { + NSAssert(!_cellSwipeViewsEnabled, @"Table model cannot be made editable when cell swipe menus are enabled"); + _canEditRows = canEditRows; +} + +- (void)setCellSwipeViewsEnabled:(BOOL)cellSwipeViewsEnabled { + NSAssert(!_canEditRows, @"Cell swipe menus cannot be enabled for editable tableModels"); + if (cellSwipeViewsEnabled) { + [self setupSwipeGestureRecognizers]; + } else { + [self removeSwipeView:YES]; + [self removeSwipeGestureRecognizers]; + } + _cellSwipeViewsEnabled = cellSwipeViewsEnabled; +} + +- (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]; + id object = [self objectForRowAtIndexPath:indexPath]; + + if (cell.frame.origin.x != 0) { + [self removeSwipeView:YES]; + return; + } + + [self removeSwipeView:NO]; + + if (cell != _swipeCell && !_animatingCellSwipe) { + [self addSwipeViewTo:cell withObject:object direction:direction]; + } + } +} + +- (void)swipeLeft:(UISwipeGestureRecognizer *)recognizer { + [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionLeft]; +} + +- (void)swipeRight:(UISwipeGestureRecognizer *)recognizer { + [self swipe:recognizer direction:UISwipeGestureRecognizerDirectionRight]; +} + +- (void)addSwipeViewTo:(UITableViewCell *)cell withObject:(id)object direction:(UISwipeGestureRecognizerDirection)direction { + if (_cellSwipeViewsEnabled) { + NSAssert(cell, @"Cannot process swipe view with nil cell"); + NSAssert(object, @"Cannot process swipe view with nil object"); + + _cellSwipeView.frame = cell.frame; + + if ([self.delegate respondsToSelector:@selector(tableController:willAddSwipeView:toCell:forObject:)]) { + [self.delegate tableController:self + willAddSwipeView:_cellSwipeView + toCell:cell + forObject:object]; + } + + [self.tableView insertSubview:_cellSwipeView belowSubview:cell]; + + _swipeCell = [cell retain]; + _swipeObject = [object retain]; + _swipeDirection = direction; + + CGRect cellFrame = cell.frame; + + _cellSwipeView.frame = CGRectMake(0, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height); + + _animatingCellSwipe = YES; + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStopAddingSwipeView:finished:context:)]; + + cell.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? cellFrame.size.width : -cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height); + [UIView commitAnimations]; + } +} + +- (void)animationDidStopAddingSwipeView:(NSString *)animationID finished:(NSNumber *)finished context:(void*)context { + _animatingCellSwipe = NO; +} + +- (void)removeSwipeView:(BOOL)animated { + if (!_cellSwipeViewsEnabled || !_swipeCell || _animatingCellSwipe) { + RKLogTrace(@"Exiting early with _cellSwipeViewsEnabled=%d, _swipCell=%@, _animatingCellSwipe=%d", + _cellSwipeViewsEnabled, _swipeCell, _animatingCellSwipe); + return; + } + + if ([self.delegate respondsToSelector:@selector(tableController:willRemoveSwipeView:fromCell:forObject:)]) { + [self.delegate tableController:self + willRemoveSwipeView:_cellSwipeView + fromCell:_swipeCell + forObject:_swipeObject]; + } + + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) { + _swipeCell.frame = CGRectMake(BOUNCE_PIXELS, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } else { + _swipeCell.frame = CGRectMake(-BOUNCE_PIXELS, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } + _animatingCellSwipe = YES; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStopOne:finished:context:)]; + [UIView commitAnimations]; + } else { + [_cellSwipeView removeFromSuperview]; + _swipeCell.frame = CGRectMake(0,_swipeCell.frame.origin.y,_swipeCell.frame.size.width, _swipeCell.frame.size.height); + [_swipeCell release]; + _swipeCell = nil; + } +} + +- (void)animationDidStopOne:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) { + _swipeCell.frame = CGRectMake(BOUNCE_PIXELS*2, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } else { + _swipeCell.frame = CGRectMake(-BOUNCE_PIXELS*2, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStopTwo:finished:context:)]; + [UIView setAnimationCurve:UIViewAnimationCurveLinear]; + [UIView commitAnimations]; +} + +- (void)animationDidStopTwo:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { + [UIView commitAnimations]; + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + if (_swipeDirection == UISwipeGestureRecognizerDirectionRight) { + _swipeCell.frame = CGRectMake(0, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } else { + _swipeCell.frame = CGRectMake(0, _swipeCell.frame.origin.y, _swipeCell.frame.size.width, _swipeCell.frame.size.height); + } + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStopThree:finished:context:)]; + [UIView setAnimationCurve:UIViewAnimationCurveLinear]; + [UIView commitAnimations]; +} + +- (void)animationDidStopThree:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { + _animatingCellSwipe = NO; + [_swipeCell release]; + _swipeCell = nil; + [_cellSwipeView removeFromSuperview]; +} + +#pragma mark UIScrollViewDelegate methods + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + [self removeSwipeView:YES]; +} + +- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { + [self removeSwipeView:NO]; + return YES; +} + +#pragma mark - Keyboard Notification methods + +- (void)resizeTableViewForKeyboard:(NSNotification *)notification { + NSAssert(_autoResizesForKeyboard, @"Errantly receiving keyboard notifications while autoResizesForKeyboard=NO"); + NSDictionary *userInfo = [notification userInfo]; + + CGRect keyboardEndFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGFloat heightForViewShift = keyboardEndFrame.size.height; + RKLogTrace(@"keyboardEndFrame.size.height=%f, heightForViewShift=%f", + keyboardEndFrame.size.height, heightForViewShift); + + CGFloat bottomBarOffset = 0.0; + 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; + 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); + } + + if ([[notification name] isEqualToString:UIKeyboardWillShowNotification]) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + UIEdgeInsets contentInsets = UIEdgeInsetsMake(0, 0, (heightForViewShift - bottomBarOffset), 0); + self.tableView.contentInset = contentInsets; + self.tableView.scrollIndicatorInsets = contentInsets; + + CGRect nonKeyboardRect = self.tableView.frame; + nonKeyboardRect.size.height -= heightForViewShift; + RKLogTrace(@"Searching for a firstResponder not inside our nonKeyboardRect (%f, %f, %f, %f)", + nonKeyboardRect.origin.x, nonKeyboardRect.origin.y, + nonKeyboardRect.size.width, nonKeyboardRect.size.height); + + UIView *firstResponder = [self.tableView findFirstResponder]; + if (firstResponder) { + CGRect firstResponderFrame = firstResponder.frame; + RKLogTrace(@"Found firstResponder=%@ at (%f, %f, %f, %f)", firstResponder, + firstResponderFrame.origin.x, firstResponderFrame.origin.y, + firstResponderFrame.size.width, firstResponderFrame.size.width); + + if (![firstResponder.superview isEqual:self.tableView]) { + firstResponderFrame = [firstResponder.superview convertRect:firstResponderFrame toView:self.tableView]; + RKLogTrace(@"firstResponder (%@) frame is not in tableView's coordinate system. Coverted to (%f, %f, %f, %f)", + firstResponder, firstResponderFrame.origin.x, firstResponderFrame.origin.y, + firstResponderFrame.size.width, firstResponderFrame.size.height); + } + + if (!CGRectContainsPoint(nonKeyboardRect, firstResponderFrame.origin)) { + RKLogTrace(@"firstResponder (%@) is underneath keyboard. Beginning scroll of tableView to show", firstResponder); + [self.tableView scrollRectToVisible:firstResponderFrame animated:YES]; + } + } + [UIView commitAnimations]; + + } else if ([[notification name] isEqualToString:UIKeyboardWillHideNotification]) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + UIEdgeInsets contentInsets = UIEdgeInsetsZero; + self.tableView.contentInset = contentInsets; + self.tableView.scrollIndicatorInsets = contentInsets; + [UIView commitAnimations]; + } +} + +- (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; + } + if ([self.delegate respondsToSelector:@selector(tableController:willLoadTableWithObjectLoader:)]) { + [self.delegate tableController:self willLoadTableWithObjectLoader:self.objectLoader]; + } + if (self.objectLoader.queue && ![self.objectLoader.queue containsRequest:self.objectLoader]) { + [self.objectLoader.queue addRequest:self.objectLoader]; + } +} + +- (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 new file mode 100644 index 0000000000..97c7b42be0 --- /dev/null +++ b/Code/UI/RKAbstractTableController_Internals.h @@ -0,0 +1,78 @@ +// +// RKAbstractTableController_Internals.h +// RestKit +// +// Created by Jeff Arena on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#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, 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. + + Responsible for finalizing loading, empty, and loaded states + and cleaning up the table overlay view. + */ +- (void)didFinishLoad; +- (void)didFailLoadWithError:(NSError *)error; + +#pragma mark - Table View Overlay + +- (void)addToOverlayView:(UIView *)view modally:(BOOL)modally; +- (void)resetOverlayView; +- (void)addSubviewOverTableView:(UIView *)view; +- (BOOL)removeImageFromOverlay:(UIImage *)image; +- (void)showImageInOverlay:(UIImage *)image; +- (void)removeImageOverlay; + +#pragma mark - Pull to Refresh Private Methods + +- (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/RKControlTableItem.h b/Code/UI/RKControlTableItem.h new file mode 100644 index 0000000000..f9f51b902b --- /dev/null +++ b/Code/UI/RKControlTableItem.h @@ -0,0 +1,45 @@ +// +// RKControlTableItem.h +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableItem.h" + +// TODO: Add note that any cell class used with an RKControlTableItem +// must have a control property to be auto-linked to the control in the table item +@interface RKControlTableItem : RKTableItem + +@property (nonatomic, retain) UIControl *control; + +/** @name Convenience Accessors */ + +@property (nonatomic, readonly) UIButton *button; +@property (nonatomic, readonly) UITextField *textField; +@property (nonatomic, readonly) UISwitch *switchControl; +@property (nonatomic, readonly) UISlider *slider; +@property (nonatomic, readonly) UILabel *label; + ++ (id)tableItemWithControl:(UIControl *)control; + +/** + Return the value from the control as an object. This will wrap + any primitive values into an appropriate object for use in mapping. + */ +- (id)controlValue; + +@end diff --git a/Code/UI/RKControlTableItem.m b/Code/UI/RKControlTableItem.m new file mode 100644 index 0000000000..c09cd4fbc1 --- /dev/null +++ b/Code/UI/RKControlTableItem.m @@ -0,0 +1,103 @@ +// +// RKControlTableItem.m +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKControlTableItem.h" +#import "RKTableViewCellMapping.h" +#import "RKControlTableViewCell.h" + +@implementation RKControlTableItem + +@synthesize control = _control; + ++ (id)tableItemWithControl:(UIControl *)control { + RKControlTableItem *tableItem = [self tableItem]; + tableItem.control = control; + return tableItem; +} + +- (id)init { + self = [super init]; + if (self) { + self.cellMapping.selectionStyle = UITableViewCellSelectionStyleNone; + self.cellMapping.accessoryType = UITableViewCellAccessoryNone; + self.cellMapping.cellClass = [RKControlTableViewCell class]; + + // Link the UITableViewCell for this table item with the control + [self.cellMapping addPrepareCellBlock:^(UITableViewCell *cell) { + if ([cell respondsToSelector:@selector(setControl:)]) { + [cell setValue:self.control forKey:@"control"]; + } + }]; + } + + return self; +} + +- (void)dealloc { + [_control release]; + [super dealloc]; +} + +- (void)setControl:(UIControl *)control { + NSAssert(control, @"Cannot add a nil control to a RKControlTableItem"); + [control retain]; + [_control release]; + _control = control; +} + +#pragma mark - Convenience Accessors + +- (UIButton *)button { + return ([self.control isKindOfClass:[UIButton class]]) ? (UIButton *) self.control : nil; +} + +- (UITextField *)textField { + return ([self.control isKindOfClass:[UITextField class]]) ? (UITextField *) self.control : nil; +} + +- (UISwitch *)switchControl { + return ([self.control isKindOfClass:[UISwitch class]]) ? (UISwitch *) self.control : nil; +} + +- (UISlider *)slider { + return ([self.control isKindOfClass:[UISlider class]]) ? (UISlider *) self.control : nil; +} + +- (UILabel *)label { + return ([self.control isKindOfClass:[UILabel class]]) ? (UILabel *) self.control : nil; +} + +// TODO: What if we replace this with a protocol that enables KVC +// via the 'controlValue' property for the common types to allow pluggability? +- (id)controlValue { + if ([self.control isKindOfClass:[UIButton class]]) { + return nil; + } else if ([self.control isKindOfClass:[UITextField class]]) { + return self.textField.text; + } else if ([self.control isKindOfClass:[UISlider class]]) { + return [NSNumber numberWithFloat:self.slider.value]; + } else if ([self.control isKindOfClass:[UISwitch class]]) { + return [NSNumber numberWithBool:self.switchControl.isOn]; + } + + return nil; +} + +@end diff --git a/Specs/CoreData/RKManagedObjectSpec.m b/Code/UI/RKControlTableViewCell.h similarity index 68% rename from Specs/CoreData/RKManagedObjectSpec.m rename to Code/UI/RKControlTableViewCell.h index 5c597ce7d5..ba7af86371 100644 --- a/Specs/CoreData/RKManagedObjectSpec.m +++ b/Code/UI/RKControlTableViewCell.h @@ -1,16 +1,16 @@ // -// RKManagedObjectSpec.m +// RKControlTableViewCell.h // RestKit // -// Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// +// Created by Blake Watters on 8/24/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. @@ -18,15 +18,10 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" -#import "RKHuman.h" +#import -@interface RKManagedObjectSpec : RKSpec { - -} - -@end +@interface RKControlTableViewCell : UITableViewCell -@implementation RKManagedObjectSpec +@property (nonatomic, retain) UIControl *control; @end diff --git a/Code/UI/RKControlTableViewCell.m b/Code/UI/RKControlTableViewCell.m new file mode 100644 index 0000000000..046419c7ba --- /dev/null +++ b/Code/UI/RKControlTableViewCell.m @@ -0,0 +1,47 @@ +// +// RKControlTableViewCell.m +// RestKit +// +// Created by Blake Watters on 8/24/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKControlTableViewCell.h" + +@implementation RKControlTableViewCell + +@synthesize control; + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (self.control.superview != self.contentView) { + [self.control removeFromSuperview]; + [self.contentView addSubview:self.control]; + } + + // If the control has not been sized, we assume you want it to + // take up the entire row of space + if (!self.control.frame.size.height) { + [self.control sizeToFit]; + CGFloat contentWidth = self.contentView.frame.size.width; + CGFloat contentHeight = self.contentView.frame.size.height; + CGFloat controlHeight = self.control.frame.size.height; + self.control.frame = CGRectMake(8, floor(contentHeight/2 - controlHeight/2), + contentWidth - 8, controlHeight); + } +} + +@end diff --git a/Code/UI/RKFetchedResultsTableController.h b/Code/UI/RKFetchedResultsTableController.h new file mode 100755 index 0000000000..127a212f47 --- /dev/null +++ b/Code/UI/RKFetchedResultsTableController.h @@ -0,0 +1,66 @@ +// +// RKFetchedResultsTableController.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKAbstractTableController.h" + +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 + */ +@interface RKFetchedResultsTableController : RKAbstractTableController { +@private + NSFetchedResultsController *_fetchedResultsController; + BOOL _showsSectionIndexTitles; + NSArray *_arraySortedFetchedObjects; + BOOL _isEmptyBeforeAnimation; +} + +@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; +@property (nonatomic, copy) RKFetchedResultsTableViewViewForHeaderInSectionBlock onViewForHeaderInSection; +@property (nonatomic, retain) NSPredicate *predicate; +@property (nonatomic, retain) NSArray *sortDescriptors; +@property (nonatomic, copy) NSString *sectionNameKeyPath; +@property (nonatomic, copy) NSString *cacheName; +@property (nonatomic, assign) BOOL showsSectionIndexTitles; +@property (nonatomic, assign) SEL sortSelector; +@property (nonatomic, copy) NSComparator sortComparator; + +- (void)setObjectMappingForClass:(Class)objectClass; +- (void)loadTable; +- (void)loadTableFromNetwork; +- (NSIndexPath *)indexPathForObject:(id)object; + +@end diff --git a/Code/UI/RKFetchedResultsTableController.m b/Code/UI/RKFetchedResultsTableController.m new file mode 100755 index 0000000000..c564f99d58 --- /dev/null +++ b/Code/UI/RKFetchedResultsTableController.m @@ -0,0 +1,644 @@ +// +// RKFetchedResultsTableController.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKFetchedResultsTableController.h" +#import "RKAbstractTableController_Internals.h" +#import "RKManagedObjectStore.h" +#import "NSManagedObject+ActiveRecord.h" +#import "RKObjectMappingOperation.h" +#import "RKManagedObjectMapping.h" +#import "RKLog.h" +#import "RKObjectMappingProvider+CoreData.h" + +// Define logging component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitUI + +@interface RKFetchedResultsTableController () +@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; +@synthesize onViewForHeaderInSection = _onViewForHeaderInSection; +@synthesize predicate = _predicate; +@synthesize sortDescriptors = _sortDescriptors; +@synthesize sectionNameKeyPath = _sectionNameKeyPath; +@synthesize cacheName = _cacheName; +@synthesize showsSectionIndexTitles = _showsSectionIndexTitles; +@synthesize sortSelector = _sortSelector; +@synthesize sortComparator = _sortComparator; +@synthesize fetchRequest = _fetchRequest; + +- (void)dealloc { + _fetchedResultsController.delegate = nil; + [_fetchedResultsController release]; + _fetchedResultsController = nil; + [_resourcePath release]; + _resourcePath = nil; + [_predicate release]; + _predicate = nil; + [_sortDescriptors release]; + _sortDescriptors = nil; + [_sectionNameKeyPath release]; + _sectionNameKeyPath = nil; + [_cacheName release]; + _cacheName = nil; + [_arraySortedFetchedObjects release]; + _arraySortedFetchedObjects = nil; + [_fetchRequest release]; + _fetchRequest = nil; + Block_release(_onViewForHeaderInSection); + Block_release(_sortComparator); + [super dealloc]; +} + +#pragma mark - Helpers + +- (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]; + BOOL success = [_fetchedResultsController performFetch:error]; + if (!success) { + 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 { + [_arraySortedFetchedObjects release]; + _arraySortedFetchedObjects = nil; + + if (_sortSelector || _sortComparator) { + if (_sortSelector) { + _arraySortedFetchedObjects = [[_fetchedResultsController.fetchedObjects sortedArrayUsingSelector:_sortSelector] retain]; + } else if (_sortComparator) { + _arraySortedFetchedObjects = [[_fetchedResultsController.fetchedObjects sortedArrayUsingComparator:_sortComparator] retain]; + } + + NSAssert(_arraySortedFetchedObjects.count == _fetchedResultsController.fetchedObjects.count, + @"sortSelector or sortComparator sort resulted in fewer objects than expected"); + } +} + +- (BOOL)isHeaderSection:(NSUInteger)section { + return (section == 0); +} + +- (BOOL)isHeaderRow:(NSUInteger)row { + BOOL isHeaderRow = NO; + NSUInteger headerItemCount = [self.headerItems count]; + if ([self isEmpty] && self.emptyItem) { + isHeaderRow = (row > 0 && row <= headerItemCount); + } else { + isHeaderRow = (row < headerItemCount); + } + return isHeaderRow; +} + +- (BOOL)isFooterSection:(NSUInteger)section { + return (section == ([self sectionCount] - 1)); +} + +- (BOOL)isFooterRow:(NSUInteger)row { + NSUInteger sectionIndex = ([self sectionCount] - 1); + id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:sectionIndex]; + NSUInteger nonFooterRowCount = [sectionInfo numberOfObjects]; + if (sectionIndex == 0) { + nonFooterRowCount += (![self isEmpty] || self.showsHeaderRowsWhenEmpty) ? [self.headerItems count] : 0; + nonFooterRowCount += ([self isEmpty] && self.emptyItem) ? 1 : 0; + } + return (row > (nonFooterRowCount - 1)); +} + +- (BOOL)isHeaderIndexPath:(NSIndexPath *)indexPath { + return ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) && + [self.headerItems count] > 0 && + [self isHeaderSection:indexPath.section] && + [self isHeaderRow:indexPath.row]); +} + +- (BOOL)isFooterIndexPath:(NSIndexPath *)indexPath { + return ((! [self isEmpty] || self.showsFooterRowsWhenEmpty) && + [self.footerItems count] > 0 && + [self isFooterSection:indexPath.section] && + [self isFooterRow:indexPath.row]); +} + +- (BOOL)isEmptySection:(NSUInteger)section { + return (section == 0); +} + +- (BOOL)isEmptyRow:(NSUInteger)row { + return (row == 0); +} + +- (BOOL)isEmptyItemIndexPath:(NSIndexPath *)indexPath { + return ([self isEmpty] && self.emptyItem && + [self isEmptySection:indexPath.section] && + [self isEmptyRow:indexPath.row]); +} + +- (NSIndexPath *)emptyItemIndexPath { + return [NSIndexPath indexPathForRow:0 inSection:0]; +} + +- (NSIndexPath *)fetchedResultsIndexPathForIndexPath:(NSIndexPath *)indexPath { + if (([self isEmpty] && self.emptyItem && + [self isEmptySection:indexPath.section] && + ! [self isEmptyRow:indexPath.row]) || + ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) && + [self.headerItems count] > 0 && + [self isHeaderSection:indexPath.section] && + ! [self isHeaderRow:indexPath.row])) { + NSUInteger adjustedRowIndex = indexPath.row; + if (![self isEmpty] || self.showsHeaderRowsWhenEmpty) { + adjustedRowIndex -= [self.headerItems count]; + } + adjustedRowIndex -= ([self isEmpty] && self.emptyItem) ? 1 : 0; + return [NSIndexPath indexPathForRow:adjustedRowIndex + inSection:indexPath.section]; + } + return indexPath; +} + +- (NSIndexPath *)indexPathForFetchedResultsIndexPath:(NSIndexPath *)indexPath { + if (([self isEmpty] && self.emptyItem && + [self isEmptySection:indexPath.section] && + ! [self isEmptyRow:indexPath.row]) || + ((! [self isEmpty] || self.showsHeaderRowsWhenEmpty) && + [self.headerItems count] > 0 && + [self isHeaderSection:indexPath.section])) { + NSUInteger adjustedRowIndex = indexPath.row; + if (![self isEmpty] || self.showsHeaderRowsWhenEmpty) { + adjustedRowIndex += [self.headerItems count]; + } + adjustedRowIndex += ([self isEmpty] && self.emptyItem) ? 1 : 0; + return [NSIndexPath indexPathForRow:adjustedRowIndex + inSection:indexPath.section]; + } + return indexPath; +} + +#pragma mark - Public + +- (NSFetchRequest *)fetchRequest { + return _fetchRequest ? _fetchRequest : _fetchedResultsController.fetchRequest; +} + +- (void)loadTable { + NSFetchRequest *fetchRequest = nil; + if (_resourcePath) { + fetchRequest = [self.objectManager.mappingProvider fetchRequestForResourcePath:self.resourcePath]; + } else { + fetchRequest = _fetchRequest; + } + NSAssert(fetchRequest != nil, @"Attempted to load RKFetchedResultsTableController with nil fetchRequest for resourcePath %@, fetchRequest %@", _resourcePath, _fetchRequest); + + if (_predicate) { + [fetchRequest setPredicate:_predicate]; + } + if (_sortDescriptors) { + [fetchRequest setSortDescriptors:_sortDescriptors]; + } + + _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest + managedObjectContext:[NSManagedObjectContext contextForCurrentThread] + sectionNameKeyPath:_sectionNameKeyPath + cacheName:_cacheName]; + _fetchedResultsController.delegate = self; + + // Perform the load + NSError *error; + [self didStartLoad]; + BOOL success = [self performFetch:&error]; + if (! success) { + [self didFailLoadWithError:error]; + } + [self updateSortedArray]; + [self.tableView reloadData]; + [self didFinishLoad]; + + if ([self isAutoRefreshNeeded] && [self isOnline] && + ![self.objectLoader isLoading] && + ![self.objectLoader.queue containsRequest:self.objectLoader]) { + [self performSelector:@selector(loadTableFromNetwork) withObject:nil afterDelay:0]; + } +} + +- (void)setSortSelector:(SEL)sortSelector { + NSAssert(_sectionNameKeyPath == nil, @"Attempted to sort fetchedObjects across multiple sections"); + NSAssert(_sortComparator == nil, @"Attempted to sort fetchedObjects with a sortSelector when a sortComparator already exists"); + _sortSelector = sortSelector; +} + +- (void)setSortComparator:(NSComparator)sortComparator { + NSAssert(_sectionNameKeyPath == nil, @"Attempted to sort fetchedObjects across multiple sections"); + NSAssert(_sortSelector == nil, @"Attempted to sort fetchedObjects with a sortComparator when a sortSelector already exists"); + if (_sortComparator) { + Block_release(_sortComparator); + _sortComparator = nil; + } + _sortComparator = Block_copy(sortComparator); +} + +- (void)setSectionNameKeyPath:(NSString*)sectionNameKeyPath { + NSAssert(_sortSelector == nil, @"Attempted to create a sectioned fetchedResultsController when a sortSelector is present"); + NSAssert(_sortComparator == nil, @"Attempted to create a sectioned fetchedResultsController when a sortComparator is present"); + [sectionNameKeyPath retain]; + [_sectionNameKeyPath release]; + _sectionNameKeyPath = sectionNameKeyPath; +} + +- (void)setResourcePath:(NSString*)resourcePath { + [_resourcePath release]; + _resourcePath = [resourcePath copy]; + self.objectLoader = [self.objectManager loaderWithResourcePath:_resourcePath]; + self.objectLoader.delegate = self; +} + +- (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]; +} + +#pragma mark - Managing Sections + +- (NSUInteger)sectionCount { + return [[_fetchedResultsController sections] count]; +} + +- (NSUInteger)rowCount { + NSUInteger fetchedItemCount = [[_fetchedResultsController fetchedObjects] count]; + NSUInteger nonFetchedItemCount = 0; + if (fetchedItemCount == 0) { + nonFetchedItemCount += self.emptyItem ? 1 : 0; + nonFetchedItemCount += self.showsHeaderRowsWhenEmpty ? [self.headerItems count] : 0; + nonFetchedItemCount += self.showsFooterRowsWhenEmpty ? [self.footerItems count] : 0; + } else { + nonFetchedItemCount += [self.headerItems count]; + nonFetchedItemCount += [self.footerItems count]; + } + return (fetchedItemCount + nonFetchedItemCount); +} + +- (UITableViewCell *)cellForObjectAtIndexPath:(NSIndexPath *)indexPath { + id mappableObject = [self objectForRowAtIndexPath:indexPath]; + NSAssert(mappableObject, @"Cannot build a tableView cell without an object"); + + 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; +} + +- (NSIndexPath *)indexPathForObject:(id)object { + if ([object isKindOfClass:[NSManagedObject class]]) { + return [self indexPathForFetchedResultsIndexPath:[_fetchedResultsController indexPathForObject:object]]; + } + return nil; +} + +- (UITableViewCell *)cellForObject:(id)object { + return [self cellForObjectAtIndexPath:[self indexPathForObject:object]]; +} + +#pragma mark - UITableViewDataSource methods + +- (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]; +} + +- (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); + id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section]; + NSUInteger numberOfRows = [sectionInfo numberOfObjects]; + + if ([self isHeaderSection:section]) { + numberOfRows += (![self isEmpty] || self.showsHeaderRowsWhenEmpty) ? [self.headerItems count] : 0; + numberOfRows += ([self isEmpty] && self.emptyItem) ? 1 : 0; + + } else if ([self isFooterSection:section]) { + numberOfRows += [self.footerItems count]; + } + return numberOfRows; +} + +- (NSString*)tableView:(UITableView*)theTableView titleForHeaderInSection:(NSInteger)section { + id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section]; + return [sectionInfo name]; +} + +- (NSString*)tableView:(UITableView*)theTableView titleForFooterInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"tableView:titleForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + return nil; +} + +- (NSArray*)sectionIndexTitlesForTableView:(UITableView*)theTableView { + 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; +} + +- (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) { + 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; + + if ([managedObject valueForKeyPath:primaryKeyAttribute]) { + RKLogTrace(@"About to fire a delete request for managedObject: %@", managedObject); + [[RKObjectManager sharedManager] deleteObject:managedObject delegate:self]; + } else { + RKLogTrace(@"About to locally delete managedObject: %@", 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 { + NSAssert(theTableView == self.tableView, @"tableView:moveRowAtIndexPath:toIndexPath: invoked with inappropriate tableView: %@", theTableView); +} + +- (BOOL)tableView:(UITableView*)theTableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:canEditRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + return self.canEditRows && [self isOnline] && !([self isHeaderIndexPath:indexPath] || [self isFooterIndexPath:indexPath] || [self isEmptyItemIndexPath:indexPath]); +} + +- (BOOL)tableView:(UITableView*)theTableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { + NSAssert(theTableView == self.tableView, @"tableView:canMoveRowAtIndexPath: invoked with inappropriate tableView: %@", theTableView); + return self.canMoveRows && !([self isHeaderIndexPath:indexPath] || [self isFooterIndexPath:indexPath] || [self isEmptyItemIndexPath:indexPath]); +} + +#pragma mark - UITableViewDelegate methods + +- (CGFloat)tableView:(UITableView*)theTableView heightForHeaderInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"heightForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); + return _heightForHeaderInSection; +} + +- (CGFloat)tableView:(UITableView*)theTableView heightForFooterInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"heightForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + return 0; +} + +- (UIView*)tableView:(UITableView*)theTableView viewForHeaderInSection:(NSInteger)section { + NSAssert(theTableView == self.tableView, @"viewForHeaderInSection: invoked with inappropriate tableView: %@", theTableView); + if (_onViewForHeaderInSection) { + NSString* sectionTitle = [self tableView:self.tableView titleForHeaderInSection:section]; + if (sectionTitle) { + return _onViewForHeaderInSection(section, sectionTitle); + } + } + return nil; +} + +- (UIView*)tableView:(UITableView*)theTableView viewForFooterInSection:(NSInteger)sectionIndex { + NSAssert(theTableView == self.tableView, @"viewForFooterInSection: invoked with inappropriate tableView: %@", theTableView); + return nil; +} + +#pragma mark - Cell Mappings + +- (id)objectForRowAtIndexPath:(NSIndexPath *)indexPath { + if ([self isEmptyItemIndexPath:indexPath]) { + return self.emptyItem; + + } else if ([self isHeaderIndexPath:indexPath]) { + NSUInteger row = ([self isEmpty] && self.emptyItem) ? (indexPath.row - 1) : indexPath.row; + return [self.headerItems objectAtIndex:row]; + + } else if ([self isFooterIndexPath:indexPath]) { + id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:indexPath.section]; + NSUInteger footerRow = (indexPath.row - sectionInfo.numberOfObjects); + if (indexPath.section == 0) { + footerRow -= (![self isEmpty] || self.showsHeaderRowsWhenEmpty) ? [self.headerItems count] : 0; + footerRow -= ([self isEmpty] && self.emptyItem) ? 1 : 0; + } + return [self.footerItems objectAtIndex:footerRow]; + + } else if (_sortSelector || _sortComparator) { + return [_arraySortedFetchedObjects objectAtIndex:[self fetchedResultsIndexPathForIndexPath:indexPath].row]; + } + return [_fetchedResultsController objectAtIndexPath:[self fetchedResultsIndexPathForIndexPath:indexPath]]; +} + +#pragma mark - Network Table Loading + +- (void)loadTableFromNetwork { + NSAssert(self.objectManager, @"Cannot perform a network load without an object manager"); + NSAssert(self.objectLoader, @"Cannot perform a network load when a network load is already in-progress"); + RKLogTrace(@"About to loadTableWithObjectLoader..."); + [self loadTableWithObjectLoader:self.objectLoader]; +} + +#pragma mark - KVO & Model States + +- (BOOL)isConsideredEmpty { + NSUInteger fetchedObjectsCount = [[_fetchedResultsController fetchedObjects] count]; + BOOL isEmpty = (fetchedObjectsCount == 0); + RKLogTrace(@"Determined isEmpty = %@. fetchedObjects count = %d", isEmpty ? @"YES" : @"NO", fetchedObjectsCount); + return isEmpty; +} + +#pragma mark - NSFetchedResultsControllerDelegate methods + +- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller { + RKLogTrace(@"Beginning updates for fetchedResultsController (%@). Current section count = %d (resource path: %@)", controller, [[controller sections] count], _resourcePath); + + if (_sortSelector) return; + + [self.tableView beginUpdates]; + _isEmptyBeforeAnimation = [self isEmpty]; +} + +- (void)controller:(NSFetchedResultsController*)controller + didChangeSection:(id)sectionInfo + atIndex:(NSUInteger)sectionIndex + forChangeType:(NSFetchedResultsChangeType)type { + + if (_sortSelector) return; + + switch (type) { + case NSFetchedResultsChangeInsert: + [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] + withRowAnimation:UITableViewRowAnimationFade]; + + if ([self.delegate respondsToSelector:@selector(tableController:didInsertSectionAtIndex:)]) { + [self.delegate tableController:self didInsertSectionAtIndex:sectionIndex]; + } + break; + + case NSFetchedResultsChangeDelete: + [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] + withRowAnimation:UITableViewRowAnimationFade]; + + if ([self.delegate respondsToSelector:@selector(tableController:didDeleteSectionAtIndex:)]) { + [self.delegate tableController:self didDeleteSectionAtIndex:sectionIndex]; + } + break; + + default: + RKLogTrace(@"Encountered unexpected section changeType: %d", type); + break; + } +} + +- (void)controller:(NSFetchedResultsController*)controller + didChangeObject:(id)anObject + atIndexPath:(NSIndexPath *)indexPath + forChangeType:(NSFetchedResultsChangeType)type + newIndexPath:(NSIndexPath *)newIndexPath { + + if (_sortSelector) return; + + NSIndexPath* adjIndexPath = [self indexPathForFetchedResultsIndexPath:indexPath]; + NSIndexPath* adjNewIndexPath = [self indexPathForFetchedResultsIndexPath:newIndexPath]; + + switch (type) { + case NSFetchedResultsChangeInsert: + [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:adjNewIndexPath] + withRowAnimation:UITableViewRowAnimationFade]; + break; + + case NSFetchedResultsChangeDelete: + [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:adjIndexPath] + withRowAnimation:UITableViewRowAnimationFade]; + break; + + case NSFetchedResultsChangeUpdate: + /** + TODO: Missing a call to a replacement for configureCell:atIndexPath: which updates + the contents of a given cell with the information from a managed object + at a given index path in the fetched results controller + */ + break; + + case NSFetchedResultsChangeMove: + [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:adjIndexPath] + withRowAnimation:UITableViewRowAnimationFade]; + [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:adjNewIndexPath] + withRowAnimation:UITableViewRowAnimationFade]; + break; + + default: + RKLogTrace(@"Encountered unexpected object changeType: %d", type); + break; + } +} + +- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller { + RKLogTrace(@"Ending updates for fetchedResultsController (%@). New section count = %d (resource path: %@)", + controller, [[controller sections] count], _resourcePath); + if (self.emptyItem && ![self isEmpty] && _isEmptyBeforeAnimation) { + [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[self emptyItemIndexPath]] + withRowAnimation:UITableViewRowAnimationFade]; + } + + [self updateSortedArray]; + + 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/RKForm.h b/Code/UI/RKForm.h new file mode 100644 index 0000000000..8405bc362d --- /dev/null +++ b/Code/UI/RKForm.h @@ -0,0 +1,117 @@ +// +// RKForm.h +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKControlTableItem.h" + +typedef enum { +// RKFormControlTypeAutodetect, // TODO: Might be nice to support auto-selection of control type + RKFormControlTypeTextField, + RKFormControlTypeTextFieldSecure, + RKFormControlTypeSwitch, + RKFormControlTypeSlider, + RKFormControlTypeLabel, + RKFormControlTypeUnknown = 1000 +} RKFormControlType; + +typedef void(^RKFormBlock)(); +@class RKTableController; +@class RKFormSection; + +@interface RKForm : NSObject { +@private + NSMutableArray *_sections; + NSMutableArray *_observedAttributes; +} + +/** + The object we are constructing a form for + */ +@property (nonatomic, readonly) id object; +// The table view we are bound to. not retained. +@property (nonatomic, readonly) RKTableController* tableController; +//@property (nonatomic, assign) id delegate; +@property (nonatomic, copy) RKFormBlock onSubmit; + +// delegates... +// formDidSumbit, formWillSubmit, formWillValidate, formDidValidateSuccessfully, formDidFailValidation:withErrors: + ++ (id)formForObject:(id)object; ++ (id)formForObject:(id)object usingBlock:(void (^)(RKForm *form))block; +- (id)initWithObject:(id)object; + +/** @name Table Item Management */ + +@property (nonatomic, readonly) NSArray *sections; +@property (nonatomic, readonly) NSArray *tableItems; + +- (void)addSection:(RKFormSection *)section; +- (void)addSectionUsingBlock:(void (^)(RKFormSection *section))block; + +/** @name Single Section Forms */ + +/** + Adds a specific table item to the form + */ +- (void)addTableItem:(RKTableItem *)tableItem; // TODO: maybe addRowWithTableItem??? + +/** @name Key Path to Control Mapping */ + +// TODO: All of these method signatures should be carefully evaluated... +// TODO: Consider renaming to addControlForAttribute: | addControlTableItemForAttribute: +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType; +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType usingBlock:(void (^)(RKControlTableItem *tableItem))block; + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control; +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control usingBlock:(void (^)(RKControlTableItem *tableItem))block; + +// TODO: Should there be a flavor that accepts UIView* and yields an RKTableItem? This would avoid needing to cast to (UIControl *) + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass; +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass usingBlock:(void (^)(RKTableItem *tableItem))block; + +- (RKTableItem *)tableItemForAttribute:(NSString *)attributeKeyPath; +- (RKControlTableItem *)controlTableItemForAttribute:(NSString *)attributeKeyPath; +- (UIControl *)controlForAttribute:(NSString *)attributeKeyPath; + +/** @name Actions */ + +// serializes the values back out of the form and into the object... +- (BOOL)commitValuesToObject; // TODO: Better method signature??? mapFormToObject.. + +/** + Submits the form b + */ +- (void)submit; + +/** + Validates the object state as represented in the form. Returns + YES if the object is state if valid. + */ +// TODO: Implement me... +//- (BOOL)validate:(NSArray**)errors; + +- (void)willLoadInTableController:(RKTableController *)tableController; +- (void)didLoadInTableController:(RKTableController *)tableController; + +// Sent from the form section +- (void)formSection:(RKFormSection *)formSection didAddTableItem:(RKTableItem *)tableItem forAttributeAtKeyPath:(NSString *)attributeKeyPath; + +@end diff --git a/Code/UI/RKForm.m b/Code/UI/RKForm.m new file mode 100644 index 0000000000..2e7a0428ac --- /dev/null +++ b/Code/UI/RKForm.m @@ -0,0 +1,314 @@ +// +// RKForm.m +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKForm.h" +#import "RKFormSection.h" +#import "RKTableViewCellMapping.h" +#import "RKTableController.h" +#import "RKObjectMappingOperation.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitUI + +@interface RKForm (Private) +- (void)removeObserverForAttributes; +@end + +@implementation RKForm + +@synthesize tableController = _tableController; +@synthesize object = _object; +@synthesize onSubmit = _onSubmit; + ++ (id)formForObject:(id)object { + return [[[self alloc] initWithObject:object] autorelease]; +} + ++ (id)formForObject:(id)object usingBlock:(void (^)(RKForm *))block { + id form = [self formForObject:object]; + if (block) block(form); + return form; +} + +- (id)initWithObject:(id)object { + if (! object) { + [NSException raise:NSInvalidArgumentException format:@"%@ - cannot initialize a form with a nil object", + NSStringFromSelector(_cmd)]; + } + + self = [self init]; + if (self) { + _object = [object retain]; + + if ([_object isKindOfClass:[NSManagedObject class]]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reloadObjectOnContextDidSaveNotification:) + name:NSManagedObjectContextDidSaveNotification + object:[(NSManagedObject *)_object managedObjectContext]]; + } + } + + return self; +} + +- (id)init { + self = [super init]; + if (self) { + _sections = [NSMutableArray new]; + _observedAttributes = [NSMutableArray new]; + } + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self removeObserverForAttributes]; + _tableController = nil; + [_object release]; + [_sections release]; + [_observedAttributes release]; + Block_release(_onSubmit); + + [super dealloc]; +} + +- (void)addSection:(RKFormSection *)section { + [_sections addObject:section]; +} + +- (void)addSectionUsingBlock:(void (^)(RKFormSection *section))block { + RKFormSection *section = [RKFormSection sectionInForm:self]; + block(section); + [self addSection:section]; +} + +#pragma mark - Table Item Management + +- (NSArray *)sections { + return [NSArray arrayWithArray:_sections]; +} + +- (RKFormSection *)returnOrInstantiateFirstSection { + if ([_sections count] > 0) { + return [_sections objectAtIndex:0]; + } + + RKFormSection *section = [RKFormSection sectionInForm:self]; + [self addSection:section]; + + return section; +} + +- (NSArray *)tableItems { + NSMutableArray *tableItems = [NSMutableArray array]; + for (RKFormSection *section in _sections) { + [tableItems addObjectsFromArray:section.objects]; + } + + return [NSArray arrayWithArray:tableItems]; +} + +#pragma mark - Proxies for Section 0 + +- (void)addTableItem:(RKTableItem *)tableItem { + [[self returnOrInstantiateFirstSection] addTableItem:tableItem]; +} + +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType usingBlock:(void (^)(RKControlTableItem *tableItem))block { + [[self returnOrInstantiateFirstSection] addRowForAttribute:attributeKeyPath withControlType:controlType usingBlock:block]; +} + +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType { + [self addRowForAttribute:attributeKeyPath withControlType:controlType usingBlock:nil]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control usingBlock:(void (^)(RKControlTableItem *tableItem))block { + [[self returnOrInstantiateFirstSection] addRowMappingAttribute:attributeKeyPath toKeyPath:controlKeyPath onControl:control usingBlock:block]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control { + [self addRowMappingAttribute:attributeKeyPath toKeyPath:controlKeyPath onControl:control usingBlock:nil]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass usingBlock:(void (^)(RKTableItem *tableItem))block { + [[self returnOrInstantiateFirstSection] addRowMappingAttribute:attributeKeyPath toKeyPath:cellKeyPath onCellWithClass:cellClass usingBlock:block]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass { + [self addRowMappingAttribute:attributeKeyPath toKeyPath:cellKeyPath onCellWithClass:cellClass usingBlock:nil]; +} + +- (RKTableItem *)tableItemForAttribute:(NSString *)attributeKeyPath { + for (RKTableItem *tableItem in self.tableItems) { + if ([[tableItem.userData valueForKey:@"__RestKit__attributeKeyPath"] isEqualToString:attributeKeyPath]) { + return tableItem; + } + } + + return nil; +} + +- (RKControlTableItem *)controlTableItemForAttribute:(NSString *)attributeKeyPath { + RKTableItem *tableItem = [self tableItemForAttribute:attributeKeyPath]; + return [tableItem isKindOfClass:[RKControlTableItem class]] ? (RKControlTableItem *) tableItem : nil; +} + +- (UIControl *)controlForAttribute:(NSString *)attributeKeyPath { + RKControlTableItem *tableItem = [self controlTableItemForAttribute:attributeKeyPath]; + return tableItem.control; +} + +#pragma mark - Actions + +// TODO: This needs thorough unit testing... +/** + TODO: There is an alternate approach for the implementation here. What we may want to do instead of tracking + the mapping like this is use KVO on the control and/or table cells that are tracking attributes and maintain an internal + dictionary of the attribute names -> values currently set on the controls. We would then just fire up the mapping operation + instead of doing this. It may be cleaner... + */ +- (BOOL)commitValuesToObject { + // Serialize the data out of the form + RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:[self.object class]]; + NSMutableDictionary *controlValues = [NSMutableDictionary dictionaryWithCapacity:[self.tableItems count]]; + for (RKTableItem *tableItem in self.tableItems) { + RKObjectAttributeMapping *controlMapping = [tableItem.userData objectForKey:@"__RestKit__attributeToControlMapping"]; + if (controlMapping) { + id controlValue = nil; + NSString *attributeKeyPath = attributeKeyPath = [controlMapping.sourceKeyPath stringByReplacingOccurrencesOfString:@"userData.__RestKit__object." withString:@""]; + NSString *controlValueKeyPath = controlMapping.destinationKeyPath; + + // TODO: Another informal protocol. Document me... + if ([tableItem isKindOfClass:[RKControlTableItem class]]) { + // Get the value out of the control and store it on the dictionary + controlValue = [tableItem performSelector:@selector(controlValue)]; + if (! controlValue) { + RKLogTrace(@"Unable to directly fetch controlValue from table item. Asking tableItem for valueForKeyPath: %@", controlValueKeyPath); + controlValue = [tableItem valueForKeyPath:controlValueKeyPath]; + } + } else { + // We are not a control cell, so we need to get the value directly from the table cell + UITableViewCell *cell = [self.tableController cellForObject:tableItem]; + NSAssert(cell, @"Attempted to serialize value out of nil table cell"); + NSAssert([cell isKindOfClass:[UITableViewCell class]], @"Expected cellForObject to return a UITableViewCell, but got a %@", NSStringFromClass([cell class])); + RKLogTrace(@"Asking cell %@ for valueForKeyPath:%@", cell, controlValueKeyPath); + controlValue = [cell valueForKeyPath:controlValueKeyPath]; + } + + RKLogTrace(@"Extracted form value for attribute '%@': %@", attributeKeyPath, controlValue); + if (controlValue) { + [controlValues setValue:controlValue forKey:attributeKeyPath]; + } + [objectMapping mapAttributes:attributeKeyPath, nil]; + } else { + // TODO: Logging!! + } + } + + RKLogTrace(@"Object mapping form state into target object '%@' with values: %@", self.object, controlValues); + objectMapping.performKeyValueValidation = NO; // TODO: Temporary... + RKObjectMappingOperation *operation = [RKObjectMappingOperation mappingOperationFromObject:controlValues toObject:self.object withMapping:objectMapping]; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + if (!success) { + RKLogWarning(@"Serialization to the target object failed with error: %@", error); + } + + return success; +} + +- (void)submit { + if ([self commitValuesToObject]) { + // TODO: Add validations? + if (self.onSubmit) self.onSubmit(); + } else { + // TODO: What to do... + } +} + +- (void)validate { + // TODO: Implement me at some point... +} + +#pragma mark - Subclass Hooks + +- (void)willLoadInTableController:(RKTableController *)tableController { +} + +- (void)didLoadInTableController:(RKTableController *)tableController { + _tableController = tableController; +} + +#pragma mark - Key Value Observing + +- (void)addObserverForAttribute:(NSString *)attributeKeyPath { + if (! [_observedAttributes containsObject:attributeKeyPath]) { + [self.object addObserver:self forKeyPath:attributeKeyPath options:NSKeyValueObservingOptionNew context:nil]; + [_observedAttributes addObject:attributeKeyPath]; + } +} + +- (void)removeObserverForAttribute:(NSString *)attributeKeyPath { + if ([_observedAttributes containsObject:attributeKeyPath]) { + [self.object removeObserver:self forKeyPath:attributeKeyPath]; + [_observedAttributes removeObject:attributeKeyPath]; + } +} + +- (void)removeObserverForAttributes { + for (NSString *keyPath in _observedAttributes) { [self.object removeObserver:self forKeyPath:keyPath]; }; + [_observedAttributes removeAllObjects]; +} + +- (void)formSection:(RKFormSection *)formSection didAddTableItem:(RKTableItem *)tableItem forAttributeAtKeyPath:(NSString *)attributeKeyPath { + [self addObserverForAttribute:attributeKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + NSAssert(object == self.object, @"Received unexpected KVO message for object that form is not bound to: %@", object); + RKLogTrace(@"Received KVO message for keyPath (%@) for object (%@)", keyPath, object); + + // TODO: We should use a notification to tell the table view about the attribute change. + // I don't like that the form knows about the tableController... + // TODO: Need to let you configure the row animations... + RKTableItem *tableItem = [self tableItemForAttribute:keyPath]; + [self.tableController reloadRowForObject:tableItem withRowAnimation:UITableViewRowAnimationFade]; +} + +- (void)reloadObjectOnContextDidSaveNotification:(NSNotification *)notification { + NSManagedObjectContext *context = (NSManagedObjectContext *) notification.object; + NSSet *deletedObjects = [notification.userInfo objectForKey:NSDeletedObjectsKey]; + NSSet *updatedObjects = [notification.userInfo objectForKey:NSUpdatedObjectsKey]; + + if ([deletedObjects containsObject:self.object]) { + RKLogWarning(@"Object was deleted while being display in a RKForm. Interface may no longer function as expected."); + [self removeObserverForAttributes]; + [_object release]; + _object = nil; + } else if ([updatedObjects containsObject:self.object]) { + RKLogDebug(@"Object was updated while being displayed in a RKForm. Refreshing..."); + [context refreshObject:_object mergeChanges:YES]; + } +} + +@end diff --git a/Code/UI/RKFormSection.h b/Code/UI/RKFormSection.h new file mode 100644 index 0000000000..327dc0e6e4 --- /dev/null +++ b/Code/UI/RKFormSection.h @@ -0,0 +1,46 @@ +// +// RKFormSection.h +// RestKit +// +// Created by Blake Watters on 8/23/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableSection.h" +#import "RKForm.h" + +@interface RKFormSection : RKTableSection + +@property (nonatomic, assign) RKForm *form; +@property (nonatomic, readonly) id object; + ++ (id)sectionInForm:(RKForm *)form; +- (id)initWithForm:(RKForm *)form; + +- (void)addTableItem:(RKTableItem *)tableItem; + +// TODO: We could get rid of the block forms and just pass nil? +// TODO: These should probably be addTableItem for clarity??? +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType; +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType usingBlock:(void (^)(RKControlTableItem *tableItem))block; + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control; +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control usingBlock:(void (^)(RKControlTableItem *tableItem))block; + +// Map an attribute to a keyPath on a particular cell class +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass; +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass usingBlock:(void (^)(RKTableItem *tableItem))block; + +@end diff --git a/Code/UI/RKFormSection.m b/Code/UI/RKFormSection.m new file mode 100644 index 0000000000..234b317490 --- /dev/null +++ b/Code/UI/RKFormSection.m @@ -0,0 +1,152 @@ +// +// RKFormSection.m +// RestKit +// +// Created by Blake Watters on 8/23/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKFormSection.h" + +@implementation RKFormSection + +@synthesize form = _form; + ++ (id)sectionInForm:(RKForm *)form { + return [[[self alloc] initWithForm:form] autorelease]; +} + +- (id)initWithForm:(RKForm *)form { + self = [super init]; + if (self) { + self.form = form; + } + + return self; +} + +- (id)object { + return self.form.object; +} + +- (void)addTableItem:(RKTableItem *)tableItem { + // We assume if you haven't configured any mappings by + // the time the item is added to the section, you probably want the defaults + if ([tableItem.cellMapping.attributeMappings count] == 0) { + [tableItem.cellMapping addDefaultMappings]; + } + // TODO: WTF? _objects is declared @protected but using _objects here fails to build... + [(NSMutableArray*)self.objects addObject:tableItem]; +} + +- (UIControl *)controlWithType:(RKFormControlType)controlType { + UIControl *control = nil; + switch (controlType) { + case RKFormControlTypeTextField: + case RKFormControlTypeTextFieldSecure:; + UITextField *textField = [[[UITextField alloc] init] autorelease]; + textField.secureTextEntry = (controlType == RKFormControlTypeTextFieldSecure); + control = (UIControl *) textField; + break; + + case RKFormControlTypeSwitch:; + control = [(UIControl *) [UISwitch new] autorelease]; + break; + + case RKFormControlTypeSlider:; + control = [(UIControl *) [UISlider new] autorelease]; + break; + + case RKFormControlTypeLabel:; + control = [(UIControl *) [UILabel new] autorelease]; + break; + + case RKFormControlTypeUnknown: + default: + break; + } + + control.backgroundColor = [UIColor clearColor]; + return control; +} + +- (NSString *)keyPathForControl:(UIControl *)control { + if ([control isKindOfClass:[UITextField class]] || + [control isKindOfClass:[UILabel class]]) { + return @"text"; + } else if ([control isKindOfClass:[UISwitch class]]) { + return @"on"; + } else if ([control isKindOfClass:[UISlider class]]) { + return @"value"; + } else { + [NSException raise:NSInvalidArgumentException format:@"*** -[%@ %@]: unable to define mapping for control type %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSStringFromClass([control class])]; + } + + return nil; +} + +- (void)addAttributeMapping:(RKObjectAttributeMapping *)attributeMapping forKeyPath:(NSString *)attributeKeyPath toTableItem:(RKTableItem *)tableItem { + [tableItem.cellMapping addAttributeMapping:attributeMapping]; + + // Use KVC storage to associate the table item with object being mapped + // TODO: Move these to constants... + [tableItem.userData setValue:self.object forKey:@"__RestKit__object"]; + [tableItem.userData setValue:attributeKeyPath forKey:@"__RestKit__attributeKeyPath"]; + [tableItem.userData setValue:attributeMapping forKey:@"__RestKit__attributeToControlMapping"]; + + [self.form formSection:self didAddTableItem:tableItem forAttributeAtKeyPath:attributeKeyPath]; + [self addTableItem:tableItem]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control usingBlock:(void (^)(RKControlTableItem *tableItem))block { + RKControlTableItem *tableItem = [RKControlTableItem tableItemWithControl:control]; + RKObjectAttributeMapping *attributeMapping = [[RKObjectAttributeMapping new] autorelease]; + attributeMapping.sourceKeyPath = [NSString stringWithFormat:@"userData.__RestKit__object.%@", attributeKeyPath]; + attributeMapping.destinationKeyPath = [NSString stringWithFormat:@"control.%@", controlKeyPath]; + + [self addAttributeMapping:attributeMapping forKeyPath:attributeKeyPath toTableItem:tableItem]; + if (block) block(tableItem); +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)controlKeyPath onControl:(UIControl *)control { + [self addRowMappingAttribute:attributeKeyPath toKeyPath:controlKeyPath onControl:control usingBlock:nil]; +} + +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType usingBlock:(void (^)(RKControlTableItem *tableItem))block { + id control = [self controlWithType:controlType]; + NSString *controlKeyPath = [self keyPathForControl:control]; + [self addRowMappingAttribute:attributeKeyPath toKeyPath:controlKeyPath onControl:control usingBlock:block]; +} + +- (void)addRowForAttribute:(NSString *)attributeKeyPath withControlType:(RKFormControlType)controlType { + [self addRowForAttribute:attributeKeyPath withControlType:controlType usingBlock:nil]; +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass usingBlock:(void (^)(RKTableItem *tableItem))block { + RKTableItem *tableItem = [RKTableItem tableItem]; + tableItem.cellMapping.cellClass = cellClass; + RKObjectAttributeMapping *attributeMapping = [[RKObjectAttributeMapping new] autorelease]; + attributeMapping.sourceKeyPath = [NSString stringWithFormat:@"userData.__RestKit__object.%@", attributeKeyPath]; + attributeMapping.destinationKeyPath = cellKeyPath; + + [self addAttributeMapping:attributeMapping forKeyPath:attributeKeyPath toTableItem:tableItem]; + if (block) block(tableItem); +} + +- (void)addRowMappingAttribute:(NSString *)attributeKeyPath toKeyPath:(NSString *)cellKeyPath onCellWithClass:(Class)cellClass { + [self addRowMappingAttribute:attributeKeyPath toKeyPath:cellKeyPath onCellWithClass:cellClass usingBlock:nil]; +} + +@end diff --git a/Code/UI/RKObjectManager+RKTableController.h b/Code/UI/RKObjectManager+RKTableController.h new file mode 100644 index 0000000000..c6d8ff43d9 --- /dev/null +++ b/Code/UI/RKObjectManager+RKTableController.h @@ -0,0 +1,60 @@ +// +// RKObjectManager+RKTableController.h +// RestKit +// +// Created by Blake Watters on 2/23/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectManager.h" + +#if TARGET_OS_IPHONE + +@class RKTableController, RKFetchedResultsTableController; + +/** + Provides extensions to RKObjectManager for instantiating RKTableController instances + */ +@interface RKObjectManager (RKTableController) + +/** + 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. + */ +- (RKTableController *)tableControllerForTableViewController:(UITableViewController *)tableViewController; + +/** + 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. + */ +- (RKTableController *)tableControllerWithTableView:(UITableView *)tableView forViewController:(UIViewController *)viewController; + +/** + 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. + */ +- (RKFetchedResultsTableController *)fetchedResultsTableControllerForTableViewController:(UITableViewController *)tableViewController; + +/** + 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. + */ +- (RKFetchedResultsTableController *)fetchedResultsTableControllerWithTableView:(UITableView *)tableView forViewController:(UIViewController *)viewController; + +@end + +#endif diff --git a/Code/UI/RKObjectManager+RKTableController.m b/Code/UI/RKObjectManager+RKTableController.m new file mode 100644 index 0000000000..b0c6b60422 --- /dev/null +++ b/Code/UI/RKObjectManager+RKTableController.m @@ -0,0 +1,43 @@ +// +// RKObjectManager+RKTableController.m +// RestKit +// +// Created by Blake Watters on 2/23/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKObjectManager+RKTableController.h" + +#if TARGET_OS_IPHONE + +#import "RKTableController.h" +#import "RKFetchedResultsTableController.h" + +@implementation RKObjectManager (RKTableController) + +- (RKTableController *)tableControllerForTableViewController:(UITableViewController *)tableViewController { + RKTableController *tableController = [RKTableController tableControllerForTableViewController:tableViewController]; + tableController.objectManager = self; + return tableController; +} + +- (RKTableController *)tableControllerWithTableView:(UITableView *)tableView forViewController:(UIViewController *)viewController { + RKTableController *tableController = [RKTableController tableControllerWithTableView:tableView forViewController:viewController]; + tableController.objectManager = self; + return tableController; +} + +- (RKFetchedResultsTableController *)fetchedResultsTableControllerForTableViewController:(UITableViewController *)tableViewController { + RKFetchedResultsTableController *tableController = [RKFetchedResultsTableController tableControllerForTableViewController:tableViewController]; + tableController.objectManager = self; + return tableController; +} + +- (RKFetchedResultsTableController *)fetchedResultsTableControllerWithTableView:(UITableView *)tableView forViewController:(UIViewController *)viewController { + RKFetchedResultsTableController *tableController = [RKFetchedResultsTableController tableControllerWithTableView:tableView forViewController:viewController]; + return tableController; +} + +@end + +#endif diff --git a/Code/UI/RKRefreshGestureRecognizer.h b/Code/UI/RKRefreshGestureRecognizer.h new file mode 100644 index 0000000000..e3759f9ee8 --- /dev/null +++ b/Code/UI/RKRefreshGestureRecognizer.h @@ -0,0 +1,43 @@ +// RKRefreshGestureRecognizer.h +// RestKit +// +// Based on PHRefreshTriggerView by Pier-Olivier Thibault +// Adapted by Gregory S. Combs on 1/13/2012 +// +// 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. +// + +#if TARGET_OS_IPHONE + +#import +#import "RKRefreshTriggerView.h" + +typedef enum { + RKRefreshIdle = 0, + RKRefreshTriggered, + RKRefreshLoading +} RKRefreshState; + +@protocol RKRefreshTriggerProtocol +@optional +- (NSDate*)pullToRefreshDataSourceLastUpdated:(UIGestureRecognizer*)recognizer; +- (BOOL)pullToRefreshDataSourceIsLoading:(UIGestureRecognizer*)recognizer; +@end + +@interface RKRefreshGestureRecognizer : UIGestureRecognizer +@property (nonatomic, assign) RKRefreshState refreshState; // You can force a gesture state by modifying this value. +@property (nonatomic, readonly) UIScrollView *scrollView; +@property (nonatomic, readonly, retain) RKRefreshTriggerView *triggerView; +@end + +#endif diff --git a/Code/UI/RKRefreshGestureRecognizer.m b/Code/UI/RKRefreshGestureRecognizer.m new file mode 100644 index 0000000000..1a101e2b2c --- /dev/null +++ b/Code/UI/RKRefreshGestureRecognizer.m @@ -0,0 +1,229 @@ +// RKRefreshGestureRecognizer.m +// RestKit +// +// Based on PHRefreshTriggerView by Pier-Olivier Thibault +// Adapted by Gregory S. Combs on 1/13/2012 +// +// 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. +// + +#if TARGET_OS_IPHONE + +#import +#import +#import "RKRefreshGestureRecognizer.h" + +NSString * const RKRefreshGestureAnimationKey = @"RKRefreshGestureAnimationKey"; +NSString * const RKRefreshResetGestureAnimationKey = @"RKRefreshResetGestureAnimationKey"; +static CGFloat const kFlipArrowAnimationTime = 0.18f; +static CGFloat const kDefaultTriggerViewHeight = 64.f; + +@interface RKRefreshGestureRecognizer () + +- (CABasicAnimation *)triggeredAnimation; +- (CABasicAnimation *)idlingAnimation; + +@property (nonatomic, retain, readwrite) RKRefreshTriggerView *triggerView; +@property (nonatomic, assign) BOOL isBoundToScrollView; +@property (nonatomic, retain) NSDateFormatter *dateFormatter; + +@end + +@implementation RKRefreshGestureRecognizer +#pragma mark - Synthesizers +@synthesize triggerView = _triggerView; +@synthesize refreshState = _refreshState; +@synthesize isBoundToScrollView = _isBoundToScrollView; +@synthesize dateFormatter = _dateFormatter; + +#pragma mark - Life Cycle +- (id)initWithTarget:(id)target action:(SEL)action { + + self = [super initWithTarget:target action:action]; + if (self) { + _triggerView = [[RKRefreshTriggerView alloc] initWithFrame:CGRectZero]; + _triggerView.titleLabel.text = NSLocalizedString(@"Pull down to refresh...", @"Pull down to refresh status"); + _dateFormatter = [[NSDateFormatter alloc] init]; + [_dateFormatter setDateStyle:NSDateFormatterShortStyle]; + [_dateFormatter setTimeStyle:NSDateFormatterShortStyle]; + [self addObserver:self forKeyPath:@"view" options:NSKeyValueObservingOptionNew context:nil]; + } + return self; +} + +- (void)dealloc { + [self removeObserver:self forKeyPath:@"view"]; + if (self.triggerView) + [self.triggerView removeFromSuperview]; + self.triggerView = nil; + [super dealloc]; +} + +#pragma mark - Utilities + +- (void)refreshLastUpdatedDate { + + SEL lastUpdatedSelector = @selector(pullToRefreshDataSourceLastUpdated:); + if (self.scrollView.delegate && [self.scrollView.delegate respondsToSelector:lastUpdatedSelector]) { + NSDate *date = [self.scrollView.delegate performSelector:lastUpdatedSelector withObject:self]; + if (!date) + return; + NSString *lastUpdatedText = [NSString stringWithFormat:@"Last Updated: %@", [self.dateFormatter stringFromDate:date]]; + self.triggerView.lastUpdatedLabel.text = lastUpdatedText; + + } else { + self.triggerView.lastUpdatedLabel.text = nil; + } + +} + +- (void)setRefreshState:(RKRefreshState)refreshState { + if (refreshState == _refreshState) + return; + + __block UIScrollView *bScrollView = self.scrollView; + + switch (refreshState) { + + case RKRefreshTriggered: { + if (![self.triggerView.arrowView.layer animationForKey:RKRefreshGestureAnimationKey]) + [self.triggerView.arrowView.layer addAnimation:[self triggeredAnimation] forKey:RKRefreshGestureAnimationKey]; + self.triggerView.titleLabel.text = NSLocalizedString(@"Release to refresh...", @"Release to refresh status"); + } + break; + + case RKRefreshIdle: { + if (_refreshState == RKRefreshLoading) { + [UIView animateWithDuration:0.2 animations:^{ + bScrollView.contentInset = UIEdgeInsetsMake(0, + bScrollView.contentInset.left, + bScrollView.contentInset.bottom, + bScrollView.contentInset.right); + }]; + + [self.triggerView.arrowView.layer removeAllAnimations]; + [self.triggerView.activityView removeFromSuperview]; + [self.triggerView.activityView stopAnimating]; + [self.triggerView addSubview:self.triggerView.arrowView]; + + } else if (_refreshState == RKRefreshTriggered) { + if ([self.triggerView.arrowView.layer animationForKey:RKRefreshGestureAnimationKey]) { + [self.triggerView.arrowView.layer addAnimation:[self idlingAnimation] forKey:RKRefreshResetGestureAnimationKey]; + } + } + [self refreshLastUpdatedDate]; + self.triggerView.titleLabel.text = NSLocalizedString(@"Pull down to refresh...", @"Pull down to refresh status"); + } + break; + + case RKRefreshLoading: { + [UIView animateWithDuration:0.2 animations:^{ + bScrollView.contentInset = UIEdgeInsetsMake(kDefaultTriggerViewHeight, + bScrollView.contentInset.left, + bScrollView.contentInset.bottom, + bScrollView.contentInset.right); + }]; + self.triggerView.titleLabel.text = NSLocalizedString(@"Loading...", @"Loading Status"); + [self.triggerView.arrowView removeFromSuperview]; + [self.triggerView addSubview:self.triggerView.activityView]; + [self.triggerView.activityView startAnimating]; + } + break; + } + + _refreshState = refreshState; +} + +- (CABasicAnimation *)triggeredAnimation { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; + animation.duration = kFlipArrowAnimationTime; + animation.toValue = [NSNumber numberWithDouble:M_PI]; + animation.fillMode = kCAFillModeForwards; + animation.removedOnCompletion = NO; + return animation; +} + +- (CABasicAnimation *)idlingAnimation { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; + animation.delegate = self; + animation.duration = kFlipArrowAnimationTime; + animation.toValue = [NSNumber numberWithDouble:0]; + animation.removedOnCompletion = YES; + return animation; +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { + [self.triggerView.arrowView.layer removeAllAnimations]; +} + +- (UIScrollView *)scrollView { + return (UIScrollView *)self.view; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + id obj = [object valueForKeyPath:keyPath]; + if (NO == [obj isKindOfClass:[UIScrollView class]]) { + self.isBoundToScrollView = NO; + return; + } + self.isBoundToScrollView = YES; + self.triggerView.frame = CGRectMake(0, -kDefaultTriggerViewHeight, CGRectGetWidth(self.view.frame), kDefaultTriggerViewHeight); + [obj addSubview:self.triggerView]; +} + +#pragma mark UIGestureRecognizer +- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer { + return NO; +} + +- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer { + return NO; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + if (!self.isBoundToScrollView) + return; + if (self.state < UIGestureRecognizerStateBegan) { + self.state = UIGestureRecognizerStateBegan; + } + + if (self.scrollView.contentOffset.y < -kDefaultTriggerViewHeight) { + self.refreshState = RKRefreshTriggered; + self.state = UIGestureRecognizerStateChanged; + } else if (self.state != UIGestureRecognizerStateRecognized) { + self.refreshState = RKRefreshIdle; + self.state = UIGestureRecognizerStateChanged; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + if (!self.isBoundToScrollView) { + self.state = UIGestureRecognizerStateFailed; + return; + } + if (self.refreshState == RKRefreshTriggered) { + self.refreshState = RKRefreshLoading; + self.state = UIGestureRecognizerStateRecognized; + return; + } + self.state = UIGestureRecognizerStateCancelled; + self.refreshState = RKRefreshIdle; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + self.state = UIGestureRecognizerStateCancelled; +} + +@end + +#endif diff --git a/Code/UI/RKRefreshTriggerView.h b/Code/UI/RKRefreshTriggerView.h new file mode 100644 index 0000000000..fd30dc6d47 --- /dev/null +++ b/Code/UI/RKRefreshTriggerView.h @@ -0,0 +1,42 @@ +// RKRefreshTriggerView.h +// RestKit +// +// Based on PHRefreshTriggerView by Pier-Olivier Thibault +// Adapted by Gregory S. Combs on 1/13/2012 +// +// 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. +// + +#if TARGET_OS_IPHONE + +#import + +@interface RKRefreshTriggerView : UIView +@property (nonatomic, retain) UILabel *titleLabel; +@property (nonatomic, retain) UILabel *lastUpdatedLabel; +@property (nonatomic, retain) UIImageView *arrowView; +@property (nonatomic, retain) UIActivityIndicatorView *activityView; + +#ifdef UI_APPEARANCE_SELECTOR +@property (nonatomic,assign) UIImage *arrowImage UI_APPEARANCE_SELECTOR; +@property (nonatomic,assign) UIActivityIndicatorViewStyle activityIndicatorStyle UI_APPEARANCE_SELECTOR; +@property (nonatomic,assign) UIFont *titleFont UI_APPEARANCE_SELECTOR; +@property (nonatomic,assign) UIColor *titleColor UI_APPEARANCE_SELECTOR; +@property (nonatomic,assign) UIFont *lastUpdatedFont UI_APPEARANCE_SELECTOR; +@property (nonatomic,assign) UIColor *lastUpdatedColor UI_APPEARANCE_SELECTOR; +@property (nonatomic,retain) UIColor *refreshBackgroundColor UI_APPEARANCE_SELECTOR; +#endif + +@end + +#endif diff --git a/Code/UI/RKRefreshTriggerView.m b/Code/UI/RKRefreshTriggerView.m new file mode 100644 index 0000000000..327d402da1 --- /dev/null +++ b/Code/UI/RKRefreshTriggerView.m @@ -0,0 +1,180 @@ +// RKRefreshTriggerView.h +// RestKit +// +// Based on PHRefreshTriggerView by Pier-Olivier Thibault +// Adapted by Gregory S. Combs on 1/13/2012 +// +// 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. +// +// TODO: Figure out how to automatically install RestKitResources.bundle and use that bundle path for arrow images + +#import "RKRefreshTriggerView.h" + +#if TARGET_OS_IPHONE + +#define DEFAULT_REFRESH_TITLE_FONT [UIFont boldSystemFontOfSize:13.0f] +#define DEFAULT_REFRESH_TITLE_COLOR [UIColor darkGrayColor] +#define DEFAULT_REFRESH_UPDATED_FONT [UIFont systemFontOfSize:12.0f] +#define DEFAULT_REFRESH_UPDATED_COLOR [UIColor lightGrayColor] +#define DEFAULT_REFRESH_ARROW_IMAGE [UIImage imageNamed:@"blueArrow"] +#define DEFAULT_REFRESH_ACTIVITY_STYLE UIActivityIndicatorViewStyleWhite + +@interface RKRefreshTriggerView () +@end + +@implementation RKRefreshTriggerView +@synthesize titleLabel = _titleLabel; +@synthesize activityView = _activityView; +@synthesize arrowView = _arrowView; +@synthesize lastUpdatedLabel = _lastUpdatedLabel; + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.backgroundColor = [UIColor clearColor]; + + _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + _titleLabel.textAlignment = UITextAlignmentCenter; + _titleLabel.backgroundColor = [UIColor clearColor]; + _titleLabel.font = DEFAULT_REFRESH_TITLE_FONT; + _titleLabel.textColor = DEFAULT_REFRESH_TITLE_COLOR; + [self addSubview:_titleLabel]; + + _lastUpdatedLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _lastUpdatedLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + _lastUpdatedLabel.backgroundColor = [UIColor clearColor]; + _lastUpdatedLabel.textAlignment = UITextAlignmentCenter; + _lastUpdatedLabel.font = DEFAULT_REFRESH_UPDATED_FONT; + _lastUpdatedLabel.textColor = DEFAULT_REFRESH_UPDATED_COLOR; + [self addSubview:_lastUpdatedLabel]; + + _arrowView = [[UIImageView alloc] initWithImage:DEFAULT_REFRESH_ARROW_IMAGE]; + _arrowView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [self addSubview:_arrowView]; + + _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:DEFAULT_REFRESH_ACTIVITY_STYLE]; + _activityView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + } + return self; +} + + +- (void)dealloc { + self.titleLabel = nil; + self.arrowView = nil; + self.activityView = nil; + self.lastUpdatedLabel = nil; + [super dealloc]; +} + + +- (void)layoutSubviews { + CGPoint imageCenter = CGPointMake(30, CGRectGetMidY(self.bounds)); + self.arrowView.center = imageCenter; + self.arrowView.frame = CGRectIntegral(self.arrowView.frame); + self.activityView.center = imageCenter; + self.titleLabel.frame = CGRectIntegral(CGRectMake(0.0f, ( CGRectGetHeight(self.bounds) * .25f ), CGRectGetWidth(self.bounds), 20.0f)); + self.lastUpdatedLabel.frame = CGRectOffset(self.titleLabel.frame, 0.f, 18.f ); +} + +#ifdef UI_APPEARANCE_SELECTOR + +#pragma mark - Proxy Accessors for UIAppearance + +- (UIImage *)arrowImage { + if (!self.arrowView) + return DEFAULT_REFRESH_ARROW_IMAGE; + return _arrowView.image; +} + +- (void)setArrowImage:(UIImage *)image { + if (!self.arrowView) + return; + self.arrowView.image = image; +} + +- (UIActivityIndicatorViewStyle)activityIndicatorStyle { + if (!self.activityView) + return DEFAULT_REFRESH_ACTIVITY_STYLE; + return self.activityView.activityIndicatorViewStyle; +} + +- (void)setActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style { + if (!self.activityView) + return; + self.activityView.activityIndicatorViewStyle = style; +} + +- (UIFont *)titleFont { + if (!self.titleLabel) + return DEFAULT_REFRESH_TITLE_FONT; + return self.titleLabel.font; +} + +- (void)setTitleFont:(UIFont *)font { + if (!self.titleLabel) + return; + self.titleLabel.font = font; +} + +- (UIColor *)titleColor { + if (!self.titleLabel) + return DEFAULT_REFRESH_TITLE_COLOR; + return self.titleLabel.textColor; +} + +- (void)setTitleColor:(UIColor *)color { + if (!self.titleLabel) + return; + self.titleLabel.textColor = color; +} + +- (UIFont *)lastUpdatedFont { + if (!self.lastUpdatedLabel) + return DEFAULT_REFRESH_UPDATED_FONT; + return self.lastUpdatedLabel.font; +} + +- (void)setLastUpdatedFont:(UIFont *)font { + if (!self.lastUpdatedLabel) + return; + self.lastUpdatedLabel.font = font; +} + +- (UIColor *)lastUpdatedColor { + if (!self.lastUpdatedLabel) + return DEFAULT_REFRESH_UPDATED_COLOR; + return self.lastUpdatedLabel.textColor; +} + +- (void)setLastUpdatedColor:(UIColor *)color { + if (!self.lastUpdatedLabel) + return; + self.lastUpdatedLabel.textColor = color; +} + +- (UIColor *)refreshBackgroundColor { + return self.backgroundColor; +} + +- (void)setRefreshBackgroundColor:(UIColor *)backgroundColor { + [self setBackgroundColor:backgroundColor]; +} +#endif + +@end + +#endif diff --git a/Code/UI/RKTableController.h b/Code/UI/RKTableController.h new file mode 100644 index 0000000000..9cf8d8ad10 --- /dev/null +++ b/Code/UI/RKTableController.h @@ -0,0 +1,173 @@ +// +// RKTableController.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if TARGET_OS_IPHONE +#import +#import "RKAbstractTableController.h" +#import "RKTableSection.h" +#import "RKTableViewCellMappings.h" +#import "RKTableItem.h" +#import "RKForm.h" +#import "RKObjectManager.h" +#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; + +/** + 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 withMapping:(RKTableViewCellMapping *)cellMapping; +- (void)loadTableItems:(NSArray *)tableItems + inSection:(NSUInteger)sectionIndex + withMapping:(RKTableViewCellMapping *)cellMapping; + +/** + Load an array of RKTableItem objects into the table using the default + RKTableViewCellMapping. An instance of the cell mapping will be created on your + behalf and configured with the default table view cell attribute mappings. + + @param tableItems An array of RKTableItem instances to load into the table + + @see RKTableItem + @see [RKTableViewCellMapping addDefaultMappings] + */ +- (void)loadTableItems:(NSArray *)tableItems; + +/** + Load an array of RKTableItem objects into the specified section with the table using the default + RKTableViewCellMapping. An instance of the cell mapping will be created on your + behalf and configured with the default table view cell attribute mappings. + + @param tableItems An array of RKTableItem instances to load into the table + @param sectionIndex The section to load the table items into. Must be less than sectionCount. + + @see RKTableItem + @see [RKTableViewCellMapping addDefaultMappings] + */ +- (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) + */ +@property (nonatomic, retain, readonly) RKForm *form; + +/** + Loads the table with the contents of the specified form object. + Forms are used to build content entry and editing interfaces for objects. + + @see RKForm + */ +- (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 +// TODO: Move to super-class? +- (void)updateTableViewUsingBlock:(void (^)())block; + +/** Adds a new section to the model. + * @param section Must be a valid non nil RKTableViewSection. */ +// NOTE: connects cellMappings if section.cellMappings is nil... +- (void)addSection:(RKTableSection *)section; + +/** 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. */ +- (void)insertSection:(RKTableSection *)section atIndex:(NSUInteger)index; + +/** Removes the specified section from the model. + * @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. */ +- (void)removeSectionAtIndex:(NSUInteger)index; + +/** Removes all sections from the model. */ +// NOTE: Adds a new section 0 +- (void)removeAllSections; +- (void)removeAllSections:(BOOL)recreateFirstSection; + +@end + +#endif // TARGET_OS_IPHONE diff --git a/Code/UI/RKTableController.m b/Code/UI/RKTableController.m new file mode 100644 index 0000000000..eef6248f4f --- /dev/null +++ b/Code/UI/RKTableController.m @@ -0,0 +1,513 @@ +// +// RKTableController.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableController.h" +#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]; + [self addSection:section]; + } + + return self; +} + +- (void)dealloc { + [self removeObserver:self forKeyPath:@"sections"]; + [_form release]; + [_sectionNameKeyPath release]; + [_sections release]; + + [super dealloc]; +} + +#pragma mark - Managing Sections + +// KVO-compliant proxy object for section mutations +- (NSMutableArray *)sectionsProxy { + return [self mutableArrayValueForKey:@"sections"]; +} + +- (void)addSectionsObject:(id)section { + [self.sections addObject:section]; +} + +- (void)insertSections:(NSArray *)objects atIndexes:(NSIndexSet *)indexes { + [self.sections insertObjects:objects atIndexes:indexes]; +} + +- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes { + [self.sections removeObjectsAtIndexes:indexes]; +} + +- (void)replaceSectionsAtIndexes:(NSIndexSet *)indexes withObjects:(NSArray *)objects { + [self.sections replaceObjectsAtIndexes:indexes withObjects:objects]; +} + +- (void)addSection:(RKTableSection *)section { + NSAssert(section, @"Cannot insert a nil section"); + section.tableController = self; + if (! section.cellMappings) { + section.cellMappings = self.cellMappings; + } + + [[self sectionsProxy] addObject:section]; +} + +- (void)removeSection:(RKTableSection *)section { + NSAssert(section, @"Cannot remove a nil section"); + if ([self.sections containsObject:section] && self.sectionCount == 1) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:@"Tables must always have at least one section" + userInfo:nil]; + } + [[self sectionsProxy] removeObject:section]; +} + +- (void)insertSection:(RKTableSection *)section atIndex:(NSUInteger)index { + NSAssert(section, @"Cannot insert a nil section"); + section.tableController = self; + [[self sectionsProxy] insertObject:section atIndex:index]; +} + +- (void)removeSectionAtIndex:(NSUInteger)index { + if (index < self.sectionCount && self.sectionCount == 1) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:@"Tables must always have at least one section" + userInfo:nil]; + } + [[self sectionsProxy] removeObjectAtIndex:index]; +} + +- (void)removeAllSections:(BOOL)recreateFirstSection { + [[self sectionsProxy] removeAllObjects]; + + if (recreateFirstSection) { + [self addSection:[RKTableSection section]]; + } +} + +- (void)removeAllSections { + [self removeAllSections:YES]; +} + +- (void)updateTableViewUsingBlock:(void (^)())block { + [self.tableView beginUpdates]; + block(); + [self.tableView endUpdates]; +} + +#pragma mark - Static Tables + +- (NSArray*)objectsWithHeaderAndFooters:(NSArray *)objects forSection:(NSUInteger)sectionIndex { + NSMutableArray* mutableObjects = [objects mutableCopy]; + if (sectionIndex == 0) { + if ([self.headerItems count] > 0) { + [mutableObjects insertObjects:self.headerItems atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.headerItems.count)]]; + } + if (self.emptyItem) { + [mutableObjects insertObject:self.emptyItem atIndex:0]; + } + } + + if (sectionIndex == (self.sectionCount - 1) && [self.footerItems count] > 0) { + [mutableObjects addObjectsFromArray:self.footerItems]; + } + + return [mutableObjects autorelease]; +} + +// 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; + + RKTableSection* section = [self sectionAtIndex:sectionIndex]; + section.objects = [self objectsWithHeaderAndFooters:objects forSection:sectionIndex]; + for (NSUInteger index = 0; index < [section.objects count]; index++) { + if ([self.delegate respondsToSelector:@selector(tableController:didInsertObject:atIndexPath:)]) { + [self.delegate tableController:self + didInsertObject:[section objectAtIndex:index] + atIndexPath:[NSIndexPath indexPathForRow:index inSection:sectionIndex]]; + } + } + + [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:self.defaultRowAnimation]; + + 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 { + [self loadObjects:objects inSection:0]; +} + +- (void)loadEmpty { + [self removeAllSections:YES]; + [self loadObjects:[NSArray array]]; +} + +- (void)loadTableItems:(NSArray *)tableItems inSection:(NSUInteger)sectionIndex { + for (RKTableItem *tableItem in tableItems) { + if ([tableItem.cellMapping.attributeMappings count] == 0) { + [tableItem.cellMapping addDefaultMappings]; + } + } + + [self loadObjects:tableItems inSection:sectionIndex]; +} + +- (void)loadTableItems:(NSArray *)tableItems + inSection:(NSUInteger)sectionIndex + withMapping:(RKTableViewCellMapping *)cellMapping { + NSAssert(tableItems, @"Cannot load a nil collection of table items"); + NSAssert(sectionIndex < self.sectionCount, @"Cannot load table items into a section that does not exist"); + NSAssert(cellMapping, @"Cannot load table items without a cell mapping"); + for (RKTableItem* tableItem in tableItems) { + tableItem.cellMapping = cellMapping; + } + [self loadTableItems:tableItems inSection:sectionIndex]; +} + +- (void)loadTableItems:(NSArray *)tableItems withMapping:(RKTableViewCellMapping *)cellMapping { + [self loadTableItems:tableItems inSection:0 withMapping:cellMapping]; +} + +- (void)loadTableItems:(NSArray *)tableItems { + [self loadTableItems:tableItems inSection:0]; +} + +#pragma mark - Network Table Loading + +- (void)loadTableFromResourcePath:(NSString*)resourcePath { + NSAssert(self.objectManager, @"Cannot perform a network load without an object manager"); + [self loadTableWithObjectLoader:[self.objectManager loaderWithResourcePath:resourcePath]]; +} + +- (void)loadTableFromResourcePath:(NSString *)resourcePath usingBlock:(void (^)(RKObjectLoader *loader))block { + RKObjectLoader* theObjectLoader = [self.objectManager loaderWithResourcePath:resourcePath]; + block(theObjectLoader); + [self loadTableWithObjectLoader:theObjectLoader]; +} + +#pragma mark - Forms + +- (void)loadForm:(RKForm *)form { + [form retain]; + [_form release]; + _form = form; + + // The form replaces the content in the table + [self removeAllSections:NO]; + + [form willLoadInTableController:self]; + for (RKFormSection *section in form.sections) { + NSUInteger sectionIndex = [form.sections indexOfObject:section]; + section.objects = [self objectsWithHeaderAndFooters:section.objects forSection:sectionIndex]; + [self addSection:(RKTableSection *)section]; + } + + [self didFinishLoad]; + [form didLoadInTableController:self]; +} + +#pragma mark - UITableViewDataSource methods + +- (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) { + if (editingStyle == UITableViewCellEditingStyleDelete) { + RKTableSection* section = [self.sections objectAtIndex:indexPath.section]; + [section removeObjectAtIndex:indexPath.row]; + + } else if (editingStyle == UITableViewCellEditingStyleInsert) { + // TODO: Anything we need to do here, since we do not have the object to insert? + } + } +} + +- (void)tableView:(UITableView*)theTableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destIndexPath { + NSAssert(theTableView == self.tableView, @"tableView:moveRowAtIndexPath:toIndexPath: invoked with inappropriate tableView: %@", theTableView); + if (self.canMoveRows) { + if (sourceIndexPath.section == destIndexPath.section) { + RKTableSection* section = [self.sections objectAtIndex:sourceIndexPath.section]; + [section moveObjectAtIndex:sourceIndexPath.row toIndex:destIndexPath.row]; + + } else { + [self.tableView beginUpdates]; + RKTableSection* sourceSection = [self.sections objectAtIndex:sourceIndexPath.section]; + id object = [[sourceSection objectAtIndex:sourceIndexPath.row] retain]; + [sourceSection removeObjectAtIndex:sourceIndexPath.row]; + + RKTableSection* destinationSection = nil; + if (destIndexPath.section < [self sectionCount]) { + destinationSection = [self.sections objectAtIndex:destIndexPath.section]; + } else { + destinationSection = [RKTableSection section]; + [self insertSection:destinationSection atIndex:destIndexPath.section]; + } + [destinationSection insertObject:object atIndex:destIndexPath.row]; + [object release]; + [self.tableView endUpdates]; + } + } +} + +#pragma mark - RKRequestDelegate & RKObjectLoaderDelegate methods + +- (void)objectLoader:(RKObjectLoader *)loader didLoadObjects:(NSArray *)objects { + // TODO: Could not get the KVO to work without a boolean property... + // TODO: Apply any sorting... + + 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]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + if ([keyPath isEqualToString:@"sections"]) { + // No table view to inform... + if (! self.tableView) { + return; + } + + NSIndexSet *changedSectionIndexes = [change objectForKey:NSKeyValueChangeIndexesKey]; + NSAssert(changedSectionIndexes, @"Received a KVO notification for settings property without an NSKeyValueChangeIndexesKey"); + if ([[change objectForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeInsertion) { + // Section(s) Inserted + [self.tableView insertSections:changedSectionIndexes withRowAnimation:self.defaultRowAnimation]; + + // TODO: Add observers on the sections objects... + + } else if ([[change objectForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeRemoval) { + // Section(s) Deleted + [self.tableView deleteSections:changedSectionIndexes withRowAnimation:self.defaultRowAnimation]; + + // TODO: Remove observers on the sections objects... + } else if ([[change objectForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeReplacement) { + // Section(s) Replaced + [self.tableView reloadSections:changedSectionIndexes withRowAnimation:self.defaultRowAnimation]; + + // TODO: Remove observers on the sections objects... + } + } + + // 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.h b/Code/UI/RKTableItem.h new file mode 100644 index 0000000000..7800d2ac07 --- /dev/null +++ b/Code/UI/RKTableItem.h @@ -0,0 +1,156 @@ +// +// RKTableItem.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKMutableBlockDictionary.h" + +@class RKTableViewCellMapping; + +/** + A generic class for defining vanilla table items when + you do not have local domain items for your table rows. This + is used to implement simple static tables quickly. + */ +@interface RKTableItem : NSObject + +@property (nonatomic, retain) NSString *text; +@property (nonatomic, retain) NSString *detailText; +@property (nonatomic, retain) UIImage *image; +@property (nonatomic, retain) NSString *URL; + +/** + A dictionary reference for storing ad-hoc KVC data useful in building + table items that require extra information beyond the concrete properties + available on the table item. + + Values stored within the userData dictionary can be used to map arbitrary data + into your table cells without resorting to subclassing RKTableItem: + [tableItem.userData setValue:userAvatarImage forKey:@"userAvatarImage"]; + [tableItem.cellMapping mapKeyPath:@"userData.userAvatarImage" toKeyPath:@"imageView.image"]; + + Note that this is an instance of RKMutableBlockDictionary -- a dictionary capable of + storing executable block values that will be resolved at mapping time. + + For convenience, you can also perform key-value coding operations on instances of RKTableItem + themselves. Any undefined KVC operations will be passed through to the underlying + userData property. This permits you to have alignment on your keyPaths between the + table item and your target cells when defining mappings. Considering the above examples, + we could also write the following code instead: + [tableItem setValue:userAvatarImage forKey:@"userAvatarImage"]; + [tableItem.cellMapping mapKeyPath:@"userAvatarImage" toKeyPath:@"imageView.image"]; + + Or more concretely, if we have a group of properties such as title, description, and publishedDate + on our UITableViewCell destination class, we can configure it quickly via: + [tableItem setValue:@"Some Title" forKey:@"title"]; + [tableItem setValue:@"This is an awesome movie." forKey:@"description"]; + [tableItem setValue:[NSDate date] forKey:@"publishedDate"]; + [tableItem.cellMapping mapAttributes:@"title", @"description", @"publishedDate", nil]; + + @see RKMutableBlockDictionary + */ +@property (nonatomic, retain) RKMutableBlockDictionary *userData; + +/** + Informal protocol implementation. Any object that responds to the `cellMapping` message + and returns an RKTableViewCellMapping will be mapped into a table view cell according to + the rules in the mapping. + + Generally table items are mapped using class -> cell mapping semantics. This is configured + via invocation of [RKTableController mapObjectClass:toTableCellClass:]. Default mappings for + RKTableItem instances are configured on your behalf when you invoke the [RKTableView loadTableItems:] + family of methods. + + If you assign a cell mapping to an individual table item then the assigned cell mapping will + be used instead of the class configured 'default' mapping. + + **Default**: nil + */ +@property (nonatomic, retain) RKTableViewCellMapping *cellMapping; + +/** + Return a new array of RKTableItem instances given a nil terminated list of strings. + Each table item will have the text property set to the string provided. + */ ++ (NSArray*)tableItemsFromStrings:(NSString *)firstString, ... NS_REQUIRES_NIL_TERMINATION; + +/** + Returns a new table item + */ ++ (id)tableItem; + +/** + Initialize a new table item and yield it to the block for configuration + */ ++ (id)tableItemUsingBlock:(void (^)(RKTableItem *tableItem))block; + +/** + Initialize a new table item with the specified text + */ ++ (id)tableItemWithText:(NSString *)text; + +/** + Initialize a new table item with the specified text & details text + */ ++ (id)tableItemWithText:(NSString *)text detailText:(NSString *)detailText; + +/** + Construct a new auto-released table item with the specified text, detailText and image + properties. + */ ++ (id)tableItemWithText:(NSString *)text detailText:(NSString *)detailText image:(UIImage *)image; + +/** + Construct a new table item with the specified text and yield it to the block for configuration. + This is a convenient mechanism for quickly constructing table items that have been subclassed. + + For example: + + NSArray* tableItems = [NSArray arrayWithObjects:[MyTableItem tableItemWithText:@"Foo" + usingBlock:^(RKTableItem *tableItem) { + [(MyTableItem *)tableItem setURL:@"app://whatever"]; + }], ...]; + */ ++ (id)tableItemWithText:(NSString *)text usingBlock:(void (^)(RKTableItem *tableItem))block; + +/** + Constructs a new table item with the specified text and URL. This is useful if you are working + with Three20 or another library that provides URL dispatching. + */ ++ (id)tableItemWithText:(NSString *)text URL:(NSString *)URL; + +/** + Construct a new table item with the specified cell mapping + */ ++ (id)tableItemWithCellMapping:(RKTableViewCellMapping *)cellMapping; + +/** + Construct a new table item that will map into an instance of the specified + UITableViewCell subclass. This is helpful if you are constructing a static table + with a handful of different cells and don't need to configure a full cell mapping. + + When invoked, an instance of RKTableViewCellMapping will be created on your behalf + and assigned to the cellMapping property. The objectClass of the cellMapping will be + set to the subclass of UITableViewCell you provided. + + @param tableViewCellSubclass A subclass of UITableViewCell to map this item into + */ ++ (id)tableItemWithCellClass:(Class)tableViewCellSubclass; + +@end diff --git a/Code/UI/RKTableItem.m b/Code/UI/RKTableItem.m new file mode 100644 index 0000000000..63ed6f2591 --- /dev/null +++ b/Code/UI/RKTableItem.m @@ -0,0 +1,143 @@ +// +// RKTableItem.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableItem.h" +#import "RKTableViewCellMapping.h" + +@implementation RKTableItem + +@synthesize text = _text; +@synthesize detailText = _detailText; +@synthesize image = _image; +@synthesize cellMapping = _cellMapping; +@synthesize URL = _URL; +@synthesize userData = _userData; + ++ (NSArray*)tableItemsFromStrings:(NSString*)firstString, ... { + va_list args; + va_start(args, firstString); + NSMutableArray* tableItems = [NSMutableArray array]; + for (NSString* string = firstString; string != nil; string = va_arg(args, NSString*)) { + RKTableItem* tableItem = [RKTableItem new]; + tableItem.text = string; + [tableItems addObject:tableItem]; + [tableItem release]; + } + va_end(args); + + return [NSArray arrayWithArray:tableItems]; +} + ++ (id)tableItem { + return [[self new] autorelease]; +} + ++ (id)tableItemUsingBlock:(void (^)(RKTableItem *))block { + RKTableItem* tableItem = [self tableItem]; + block(tableItem); + return tableItem; +} + ++ (id)tableItemWithText:(NSString *)text { + return [self tableItemUsingBlock:^(RKTableItem *tableItem) { + tableItem.text = text; + }]; +} + ++ (id)tableItemWithText:(NSString *)text detailText:(NSString *)detailText { + return [self tableItemUsingBlock:^(RKTableItem *tableItem) { + tableItem.text = text; + tableItem.detailText = detailText; + }]; +} + ++ (id)tableItemWithText:(NSString *)text detailText:(NSString *)detailText image:(UIImage*)image { + RKTableItem* tableItem = [self new]; + tableItem.text = text; + tableItem.detailText = detailText; + tableItem.image = image; + + return [tableItem autorelease]; +} + ++ (id)tableItemWithText:(NSString *)text usingBlock:(void (^)(RKTableItem *))block { + RKTableItem* tableItem = [[self new] autorelease]; + tableItem.text = text; + block(tableItem); + return tableItem; +} + ++ (id)tableItemWithText:(NSString *)text URL:(NSString *)URL { + RKTableItem* tableItem = [self tableItem]; + tableItem.text = text; + tableItem.URL = URL; + return tableItem; +} + ++ (id)tableItemWithCellMapping:(RKTableViewCellMapping *)cellMapping { + RKTableItem *tableItem = [self tableItem]; + tableItem.cellMapping = cellMapping; + + return tableItem; +} + ++ (id)tableItemWithCellClass:(Class)tableViewCellSubclass { + RKTableItem *tableItem = [self tableItem]; + tableItem.cellMapping = [RKTableViewCellMapping cellMapping]; + tableItem.cellMapping.cellClass = tableViewCellSubclass; + + return tableItem; +} + +- (id)init { + self = [super init]; + if (self) { + _userData = [RKMutableBlockDictionary new]; + _cellMapping = [RKTableViewCellMapping new]; + } + + return self; +} + +- (void)dealloc { + [_text release]; + [_detailText release]; + [_image release]; + [_cellMapping release]; + [_userData release]; + + [super dealloc]; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"<%@: %p text=%@, detailText=%@, image=%p>", NSStringFromClass([self class]), self, self.text, self.detailText, self.image]; +} + +#pragma mark - User Data KVC Proxy + +- (void)setValue:(id)value forUndefinedKey:(NSString *)key { + [self.userData setValue:value ? value : [NSNull null] forKey:key]; +} + +- (id)valueForUndefinedKey:(NSString *)key { + return [self.userData valueForKey:key]; +} + +@end diff --git a/Code/UI/RKTableSection.h b/Code/UI/RKTableSection.h new file mode 100644 index 0000000000..86ec5bf6a7 --- /dev/null +++ b/Code/UI/RKTableSection.h @@ -0,0 +1,61 @@ +// +// RKTableViewSection.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKObjectMapping.h" +#import "RKTableViewCellMappings.h" + +@class RKTableController; + +@interface RKTableSection : NSObject { + @protected + NSMutableArray *_objects; +} + +// Basics +@property (nonatomic, assign) RKTableController* tableController; +@property (nonatomic, readonly) UITableView* tableView; + +// Object Mapping Table Stuff +@property (nonatomic, retain) NSArray* objects; +@property (nonatomic, retain) RKTableViewCellMappings* cellMappings; + +// Header & Footer Views, etc. +@property (nonatomic, retain) NSString* headerTitle; +@property (nonatomic, retain) NSString* footerTitle; +@property (nonatomic, assign) CGFloat headerHeight; +@property (nonatomic, assign) CGFloat footerHeight; +@property (nonatomic, retain) UIView* headerView; +@property (nonatomic, retain) UIView* footerView; + +// number of cells in the section +@property (nonatomic, readonly) NSUInteger rowCount; + ++ (id)section; ++ (id)sectionUsingBlock:(void (^)(RKTableSection *))block; ++ (id)sectionForObjects:(NSArray*)objects withMappings:(RKTableViewCellMappings*)cellMappings; + +- (id)objectAtIndex:(NSUInteger)rowIndex; +- (void)insertObject:(id)object atIndex:(NSUInteger)index; +- (void)removeObjectAtIndex:(NSUInteger)index; +- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object; +- (void)moveObjectAtIndex:(NSUInteger)sourceIndex toIndex:(NSUInteger)destinationIndex; + +@end diff --git a/Code/UI/RKTableSection.m b/Code/UI/RKTableSection.m new file mode 100644 index 0000000000..73f9627c74 --- /dev/null +++ b/Code/UI/RKTableSection.m @@ -0,0 +1,155 @@ +// +// RKTableViewSection.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableSection.h" +#import "RKTableController.h" +#import "RKTableViewCellMapping.h" +#import "RKLog.h" + +// Define logging component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitUI + +@implementation RKTableSection + +@synthesize objects = _objects; +@synthesize cellMappings = _cellMappings; +@synthesize tableController = _tableController; +@synthesize headerTitle = _headerTitle; +@synthesize footerTitle = _footerTitle; +@synthesize headerHeight = _headerHeight; +@synthesize footerHeight = _footerHeight; +@synthesize headerView = _headerView; +@synthesize footerView = _footerView; + ++ (id)section { + return [[self new] autorelease]; +} + ++ (id)sectionUsingBlock:(void (^)(RKTableSection *))block { + RKTableSection* section = [self section]; + block(section); + return section; +} + ++ (id)sectionForObjects:(NSArray *)objects withMappings:(RKTableViewCellMappings *)cellMappings { + return [self sectionUsingBlock:^(RKTableSection *section) { + section.objects = objects; + section.cellMappings = cellMappings; + }]; +} + +- (id)init { + self = [super init]; + if (self) { + _objects = [NSMutableArray new]; + _headerHeight = 0; + _footerHeight = 0; + } + + return self; +} + +- (void)dealloc { + [_objects release]; + [_cellMappings release]; + [_headerTitle release]; + [_footerTitle release]; + [_headerView release]; + [_footerView release]; + [super dealloc]; +} + +- (void)setObjects:(NSArray *)objects { + if (! [objects isMemberOfClass:[NSMutableArray class]]) { + NSMutableArray* mutableObjects = [objects mutableCopy]; + [_objects release]; + _objects = mutableObjects; + } else { + [objects retain]; + [_objects release]; + _objects = (NSMutableArray *) objects; + } +} + +- (NSUInteger)rowCount { + return [_objects count]; +} + +- (id)objectAtIndex:(NSUInteger)rowIndex { + return [_objects objectAtIndex:rowIndex]; +} + +- (UITableView*)tableView { + return _tableController.tableView; +} + +- (void)insertObject:(id)object atIndex:(NSUInteger)index { + [(NSMutableArray*)_objects insertObject:object atIndex:index]; + + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:index + inSection:[_tableController indexForSection:self]]; + [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] + withRowAnimation:_tableController.defaultRowAnimation]; + + if ([_tableController.delegate respondsToSelector:@selector(tableController:didInsertObject:atIndexPath:)]) { + [_tableController.delegate tableController:_tableController didInsertObject:object atIndexPath:indexPath]; + } +} + +- (void)removeObjectAtIndex:(NSUInteger)index { + id object = [self objectAtIndex:index]; + [(NSMutableArray*)_objects removeObjectAtIndex:index]; + + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:index + inSection:[_tableController indexForSection:self]]; + [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] + withRowAnimation:_tableController.defaultRowAnimation]; + + if ([_tableController.delegate respondsToSelector:@selector(tableController:didDeleteObject:atIndexPath:)]) { + [_tableController.delegate tableController:_tableController didDeleteObject:object atIndexPath:indexPath]; + } +} + +- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object { + [(NSMutableArray*)_objects replaceObjectAtIndex:index withObject:object]; + + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:index + inSection:[_tableController indexForSection:self]]; + [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] + withRowAnimation:_tableController.defaultRowAnimation]; + + if ([_tableController.delegate respondsToSelector:@selector(tableController:didUpdateObject:atIndexPath:)]) { + [_tableController.delegate tableController:_tableController didUpdateObject:object atIndexPath:indexPath]; + } +} + +- (void)moveObjectAtIndex:(NSUInteger)sourceIndex toIndex:(NSUInteger)destinationIndex { + [self.tableView beginUpdates]; + id object = [[self objectAtIndex:sourceIndex] retain]; + [self removeObjectAtIndex:sourceIndex]; + [self insertObject:object atIndex:destinationIndex]; + [object release]; + [self.tableView endUpdates]; + + // TODO: Should use moveRowAtIndexPath: when on iOS 5 +} + +@end diff --git a/Code/UI/RKTableViewCellMapping.h b/Code/UI/RKTableViewCellMapping.h new file mode 100644 index 0000000000..573d08239f --- /dev/null +++ b/Code/UI/RKTableViewCellMapping.h @@ -0,0 +1,246 @@ +// +// RKTableViewCellMapping.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKObjectMapping.h" + +/** @name Cell Mapping Block Callbacks **/ + +typedef void(^RKTableViewCellForObjectAtIndexPathBlock)(UITableViewCell *cell, id object, NSIndexPath *indexPath); +typedef CGFloat(^RKTableViewHeightOfCellForObjectAtIndexPathBlock)(id object, NSIndexPath *indexPath); +typedef void(^RKTableViewAccessoryButtonTappedForObjectAtIndexPathBlock)(UITableViewCell *cell, id object, NSIndexPath *indexPath); +typedef NSString*(^RKTableViewTitleForDeleteButtonForObjectAtIndexPathBlock)(UITableViewCell *cell, id object, NSIndexPath *indexPath); +typedef UITableViewCellEditingStyle(^RKTableViewEditingStyleForObjectAtIndexPathBlock)(UITableViewCell *cell, id object, NSIndexPath *indexPath); +typedef NSIndexPath*(^RKTableViewTargetIndexPathForMoveBlock)(UITableViewCell *cell, id object, NSIndexPath *sourceIndexPath, NSIndexPath *destIndexPath); +typedef void(^RKTableViewAnonymousBlock)(); +typedef void(^RKTableViewCellBlock)(UITableViewCell *cell); + +/** + Defines a RestKit object mapping suitable for mapping generic + objects into UITableViewCell derived classes or cells loaded from + NIBs. The cell mapping leverages RestKit's object mapping engine to + dynamically map keyPaths in your object model into properties on the + table cell view. + + Cell mappings are used to drive table view cells within an RKTableController + derived class. The cell mapping does not require any specific implementation + on the target cell classes beyond exposure of the configurable UIView's via + KVC properties. + + @see RKTableController + */ +@interface RKTableViewCellMapping : RKObjectMapping { +@protected + NSMutableArray *_prepareCellBlocks; +} + +/** + The UITableViewCell subclass that this mapping will target. This + is an alias for the objectClass property defined on the base mapping + provided here to make things more explicit. + + @default [GGImageButtonTableViewCell class] + @see objectClass + */ +@property (nonatomic, assign) Class cellClass; + +/** + Convenience accessor for setting the cellClass attribute via a string + rather than a class instance. This will typically save you from having + to #import the header file for your target cells in your table view controller + + @default @"GGImageButtonTableViewCell" + @see cellClass + */ +@property (nonatomic, assign) NSString* cellClassName; + +/** + A reuse identifier for cells created using this mapping. These cells will be + dequeued and reused within the table view for optimal performance. By default, + a reuseIdentifier is set for you when you assign an object class to the mapping. + You can override this behavior if you have multiple cells representing the same types + of objects within the table view and need to pool the cells differently. + + @default NSStringFromClass(self.objectClass) + */ +@property (nonatomic, retain) NSString* reuseIdentifier; + +/** + 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; + +/** + The cell style to use for cells created with this mapping + + @default UITableViewCellStyleDefault + */ +@property (nonatomic, assign) UITableViewCellStyle style; + +/** + The cell accessory type to use for cells created with this mapping + + @default UITableViewCellAccessoryNone + */ +@property (nonatomic, assign) UITableViewCellAccessoryType accessoryType; + +/** + The cell selection style to use for cells created with this mapping + + @default UITableViewCellSelectionStyleBlue + */ +@property (nonatomic, assign) UITableViewCellSelectionStyle selectionStyle; + +/** + Whether the tableController should call deselectRowAtIndexPath:animated: + on the tableView when a cell is selected. + + @default YES + */ +@property (nonatomic, assign) BOOL deselectsRowOnSelection; + +/** + The row height to use for cells created with this mapping. + Use of this property requires that RKTableController instance you are + using the mapping to build cells for has been configured with variableHeightRows = YES + + This value is mutually exclusive of the heightOfCellForObjectAtIndexPath property + and will be ignored if you assign a block to perform dynamic row height calculations. + + **Default**: 44 + */ +@property (nonatomic, assign) CGFloat rowHeight; + +/** @name Cell Events **/ + +/** + Invoked when the user has touched a cell corresponding to an object. The block + is invoked with a reference to both the UITableViewCell that was touched and the + object the cell is representing. + */ +@property (nonatomic, copy) RKTableViewCellForObjectAtIndexPathBlock onSelectCellForObjectAtIndexPath; + +/** + Invoked when the user has touched a cell configured with this mapping. The block is invoked + without any arguments. This is useful for one-off touch events where you do not care about + the content in which the selection took place. + + @see onSelectCellForObjectAtIndexPath + */ +@property (nonatomic, copy) RKTableViewAnonymousBlock onSelectCell; + +/** + A block to invoke when a table view cell created with this mapping is going to appear in the table. + The block will be invoked with the UITableViewCell, an id reference to the mapped object being + represented in the cell, and the NSIndexPath for the row position the cell will be appearing at. + + This is a good moment to perform any customization to the cell before it becomes visible in the table view. + */ +@property (nonatomic, copy) RKTableViewCellForObjectAtIndexPathBlock onCellWillAppearForObjectAtIndexPath; + +/** + A block to invoke when the table view is measuring the height of the UITableViewCell. + The block will be invoked with the UITableViewCell, an id reference to the mapped object being + represented in the cell, and the NSIndexPath for the row position the cell will be appearing at. + */ +@property (nonatomic, copy) RKTableViewHeightOfCellForObjectAtIndexPathBlock heightOfCellForObjectAtIndexPath; + +/** + A block to invoke when the accessory button for a given cell is tapped by the user. + The block will be invoked with the UITableViewCell, an id reference to the mapped object being + represented in the cell, and the NSIndexPath for the row position the cell will be appearing at. + */ +@property (nonatomic, copy) RKTableViewAccessoryButtonTappedForObjectAtIndexPathBlock onTapAccessoryButtonForObjectAtIndexPath; + +/** + A block to invoke when the table view is determining the title for the delete confirmation button. + The block will be invoked with the UITableViewCell, an id reference to the mapped object being + represented in the cell, and the NSIndexPath for the row position the cell will be appearing at. + */ +@property (nonatomic, copy) RKTableViewTitleForDeleteButtonForObjectAtIndexPathBlock titleForDeleteButtonForObjectAtIndexPath; + +/** + A block to invoke when the table view is determining the editing style for a given row. + The block will be invoked with the UITableViewCell, an id reference to the mapped object being + represented in the cell, and the NSIndexPath for the row position the cell will be appearing at. + */ +@property (nonatomic, copy) RKTableViewEditingStyleForObjectAtIndexPathBlock editingStyleForObjectAtIndexPath; + +@property (nonatomic, copy) RKTableViewTargetIndexPathForMoveBlock targetIndexPathForMove; + +/** + Returns a new auto-released mapping targeting UITableViewCell + */ ++ (id)cellMapping; + +/** + Returns a new auto-released mapping targeting UITableViewCell with the specified reuseIdentifier + */ ++ (id)cellMappingForReuseIdentifier:(NSString *)reuseIdentifier; + +/** + Creates and returns an RKTableCellMapping instance configured with the default cell mappings. + + @return An RKTableCellMapping instance with default mappings applied. + @see [RKTableCellMapping addDefaultMappings] + */ ++ (id)defaultCellMapping; + +/** + Returns a new auto-released object mapping targeting UITableViewCell. The mapping + will be yielded to the block for configuration. + */ ++ (id)cellMappingUsingBlock:(void (^)(RKTableViewCellMapping *cellMapping))block; + +/** + Sets up default mappings connecting common properties to their UITableViewCell counterparts as follows: + + [self mapKeyPath:@"text" toAttribute:@"textLabel.text"]; + [self mapKeyPath:@"detailText" toAttribute:@"detailTextLabel.text"]; + [self mapKeyPath:@"image" toAttribute:@"imageView.image"]; + + These properties are exposed on the RKTableItem class for convenience in quickly building static + table views/ + + @see RKTableItem + */ +- (void)addDefaultMappings; + +/** + Configure a block to be invoked whenever a cell is prepared for use with this mapping. + The block will be invoked each time a cell is either initialized or dequeued for reuse. + */ +- (void)addPrepareCellBlock:(void (^)(UITableViewCell *cell))block; + +/** @name Configuring Control Actions */ +// TODO: Docs!!! + +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents toControlAtKeyPath:(NSString *)keyPath; +- (void)addTarget:(id)target action:(SEL)action forTouchEventToControlAtKeyPath:(NSString *)keyPath; +- (void)addBlockAction:(void (^)(id sender))block forControlEvents:(UIControlEvents)controlEvents toControlAtKeyPath:(NSString *)keyPath; +- (void)addBlockAction:(void (^)(id sender))block forTouchEventToControlAtKeyPath:(NSString *)keyPath; + +@end diff --git a/Code/UI/RKTableViewCellMapping.m b/Code/UI/RKTableViewCellMapping.m new file mode 100644 index 0000000000..dd579c4162 --- /dev/null +++ b/Code/UI/RKTableViewCellMapping.m @@ -0,0 +1,284 @@ +// +// RKTableViewCellMapping.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableViewCellMapping.h" +#import "RKLog.h" + +// Define logging component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitUI + +/** + A simple class for wrapping blocks into target/action + invocations that can be used with UIControl events + */ +typedef void(^RKControlBlockActionBlock)(id sender); +@interface RKControlBlockAction : NSObject { +@private + RKControlBlockActionBlock _actionBlock; +} + +@property (nonatomic, readonly) SEL actionSelector; + ++ (id)actionWithBlock:(void(^)(id sender))block; +- (id)initWithBlock:(void(^)(id sender))block; + +/** + The target action to use when wrapping a block + */ +- (void)actionForControlEvent:(id)sender; + +@end + +@implementation RKControlBlockAction + ++ (id)actionWithBlock:(void(^)(id sender))block { + return [[[self alloc] initWithBlock:block] autorelease]; +} + +- (id)initWithBlock:(void(^)(id sender))block { + self = [self init]; + if (self) { + _actionBlock = Block_copy(block); + } + + return self; +} + +- (void)actionForControlEvent:(id)sender { + _actionBlock(sender); +} + +- (SEL)actionSelector { + return @selector(actionForControlEvent:); +} + +- (void)dealloc { + Block_release(_actionBlock); + [super dealloc]; +} + +@end + +@implementation RKTableViewCellMapping + +@synthesize reuseIdentifier = _reuseIdentifier; +@synthesize style = _style; +@synthesize accessoryType = _accessoryType; +@synthesize selectionStyle = _selectionStyle; +@synthesize onSelectCellForObjectAtIndexPath = _onSelectCellForObjectAtIndexPath; +@synthesize onSelectCell = _onSelectCell; +@synthesize onCellWillAppearForObjectAtIndexPath = _onCellWillAppearForObjectAtIndexPath; +@synthesize heightOfCellForObjectAtIndexPath = _heightOfCellForObjectAtIndexPath; +@synthesize onTapAccessoryButtonForObjectAtIndexPath = _onTapAccessoryButtonForObjectAtIndexPath; +@synthesize titleForDeleteButtonForObjectAtIndexPath = _titleForDeleteButtonForObjectAtIndexPath; +@synthesize editingStyleForObjectAtIndexPath = _editingStyleForObjectAtIndexPath; +@synthesize targetIndexPathForMove = _targetIndexPathForMove; +@synthesize rowHeight = _rowHeight; +@synthesize deselectsRowOnSelection = _deselectsRowOnSelection; +@synthesize managesCellAttributes; + ++ (id)cellMapping { + return [self mappingForClass:[UITableViewCell class]]; +} + ++ (id)cellMappingForReuseIdentifier:(NSString *)reuseIdentifier { + RKTableViewCellMapping *cellMapping = [self cellMapping]; + cellMapping.reuseIdentifier = reuseIdentifier; + return cellMapping; +} + ++ (id)defaultCellMapping { + RKTableViewCellMapping *cellMapping = [self cellMapping]; + [cellMapping addDefaultMappings]; + return cellMapping; +} + ++ (id)cellMappingUsingBlock:(void (^)(RKTableViewCellMapping*))block { + RKTableViewCellMapping* cellMapping = [self cellMapping]; + block(cellMapping); + return cellMapping; +} + +- (id)init { + self = [super init]; + if (self) { + self.cellClass = [UITableViewCell class]; + self.style = UITableViewCellStyleDefault; + self.managesCellAttributes = NO; + _accessoryType = UITableViewCellAccessoryNone; + _selectionStyle = UITableViewCellSelectionStyleBlue; + self.rowHeight = 44; + self.deselectsRowOnSelection = YES; + _prepareCellBlocks = [NSMutableArray new]; + } + + return self; +} + +- (void)addDefaultMappings { + [self mapKeyPath:@"text" toAttribute:@"textLabel.text"]; + [self mapKeyPath:@"detailText" toAttribute:@"detailTextLabel.text"]; + [self mapKeyPath:@"image" toAttribute:@"imageView.image"]; +} + +- (void)dealloc { + [_reuseIdentifier release]; + [_prepareCellBlocks release]; + Block_release(_onSelectCell); + Block_release(_onSelectCellForObjectAtIndexPath); + Block_release(_onCellWillAppearForObjectAtIndexPath); + Block_release(_heightOfCellForObjectAtIndexPath); + Block_release(_onTapAccessoryButtonForObjectAtIndexPath); + Block_release(_titleForDeleteButtonForObjectAtIndexPath); + Block_release(_editingStyleForObjectAtIndexPath); + Block_release(_targetIndexPathForMove); + [super dealloc]; +} + +- (NSMutableArray *)prepareCellBlocks { + return _prepareCellBlocks; +} + +- (id)copyWithZone:(NSZone *)zone { + RKTableViewCellMapping *copy = [super copyWithZone:zone]; + copy.reuseIdentifier = self.reuseIdentifier; + copy.style = self.style; + copy.accessoryType = self.accessoryType; + copy.selectionStyle = self.selectionStyle; + copy.onSelectCellForObjectAtIndexPath = self.onSelectCellForObjectAtIndexPath; + copy.onSelectCell = self.onSelectCell; + copy.onCellWillAppearForObjectAtIndexPath = self.onCellWillAppearForObjectAtIndexPath; + copy.heightOfCellForObjectAtIndexPath = self.heightOfCellForObjectAtIndexPath; + copy.onTapAccessoryButtonForObjectAtIndexPath = self.onTapAccessoryButtonForObjectAtIndexPath; + copy.titleForDeleteButtonForObjectAtIndexPath = self.titleForDeleteButtonForObjectAtIndexPath; + copy.editingStyleForObjectAtIndexPath = self.editingStyleForObjectAtIndexPath; + copy.targetIndexPathForMove = self.targetIndexPathForMove; + copy.rowHeight = self.rowHeight; + + @synchronized(_prepareCellBlocks) { + for (void (^block)(UITableViewCell *) in _prepareCellBlocks) { + void (^blockCopy)(UITableViewCell *cell) = [block copy]; + [copy addPrepareCellBlock:blockCopy]; + [blockCopy release]; + } + } + + return copy; +} + + +- (id)mappableObjectForData:(UITableView *)tableView { + NSAssert([tableView isKindOfClass:[UITableView class]], @"Expected to be invoked with a tableView as the data. Got %@", tableView); + RKLogTrace(@"About to dequeue reusable cell using self.reuseIdentifier=%@", self.reuseIdentifier); + UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier]; + if (! cell) { + cell = [[[self.objectClass alloc] initWithStyle:self.style + reuseIdentifier:self.reuseIdentifier] autorelease]; + } + + if (self.managesCellAttributes) { + cell.accessoryType = self.accessoryType; + cell.selectionStyle = self.selectionStyle; + } + + // Fire the prepare callbacks + for (void (^block)(UITableViewCell *) in _prepareCellBlocks) { + block(cell); + } + + return cell; +} + +- (void)setSelectionStyle:(UITableViewCellSelectionStyle)selectionStyle { + self.managesCellAttributes = YES; + _selectionStyle = selectionStyle; +} + +- (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType { + self.managesCellAttributes = YES; + _accessoryType = accessoryType; +} + +- (void)setObjectClass:(Class)objectClass { + NSAssert([objectClass isSubclassOfClass:[UITableViewCell class]], @"Cell mappings can only target classes that inherit from UITableViewCell"); + [super setObjectClass:objectClass]; +} + +- (void)setCellClass:(Class)cellClass { + [self setObjectClass:cellClass]; +} + +- (NSString*)cellClassName { + return NSStringFromClass(self.cellClass); +} + +- (void)setCellClassName:(NSString *)cellClassName { + self.cellClass = NSClassFromString(cellClassName); +} + +- (Class)cellClass { + return [self objectClass]; +} + +- (NSString *)reuseIdentifier { + return _reuseIdentifier ? _reuseIdentifier : NSStringFromClass(self.objectClass); +} + +#pragma mark - Control Action Helpers + +- (void)addPrepareCellBlock:(void (^)(UITableViewCell *cell))block { + void (^blockCopy)(UITableViewCell *cell) = [block copy]; + [_prepareCellBlocks addObject:blockCopy]; + [blockCopy release]; +} + +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents toControlAtKeyPath:(NSString *)keyPath { + [self addPrepareCellBlock:^(UITableViewCell *cell) { + UIControl *control = [cell valueForKeyPath:keyPath]; + if (control) { + [control addTarget:target action:action forControlEvents:controlEvents]; + } else { + // TODO: Logging... + } + }]; +} + +- (void)addTarget:(id)target action:(SEL)action forTouchEventToControlAtKeyPath:(NSString *)keyPath { + [self addTarget:target action:action forControlEvents:UIControlEventTouchUpInside toControlAtKeyPath:keyPath]; +} + +- (void)addBlockAction:(void (^)(id sender))block forControlEvents:(UIControlEvents)controlEvents toControlAtKeyPath:(NSString *)keyPath { + [self addPrepareCellBlock:^(UITableViewCell *cell) { + RKControlBlockAction *blockAction = [RKControlBlockAction actionWithBlock:block]; + UIControl *control = [cell valueForKeyPath:keyPath]; + if (control) { + [control addTarget:blockAction action:blockAction.actionSelector forControlEvents:controlEvents]; + } else { + // TODO: Logging... + } + }]; +} + +- (void)addBlockAction:(void (^)(id sender))block forTouchEventToControlAtKeyPath:(NSString *)keyPath { + [self addBlockAction:block forControlEvents:UIControlEventTouchUpInside toControlAtKeyPath:keyPath]; +} + +@end diff --git a/Code/UI/RKTableViewCellMappings.h b/Code/UI/RKTableViewCellMappings.h new file mode 100644 index 0000000000..2426943a77 --- /dev/null +++ b/Code/UI/RKTableViewCellMappings.h @@ -0,0 +1,33 @@ +// +// RKTableViewCellMappings.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableViewCellMapping.h" + +@interface RKTableViewCellMappings : NSObject { + @private + NSMutableDictionary* _cellMappings; +} + ++ (id)cellMappings; +- (void)setCellMapping:(RKTableViewCellMapping*)cellMapping forClass:(Class)objectClass; +- (RKTableViewCellMapping*)cellMappingForClass:(Class)objectClass; +- (RKTableViewCellMapping*)cellMappingForObject:(id)object; + +@end diff --git a/Code/UI/RKTableViewCellMappings.m b/Code/UI/RKTableViewCellMappings.m new file mode 100644 index 0000000000..a7ddbde93c --- /dev/null +++ b/Code/UI/RKTableViewCellMappings.m @@ -0,0 +1,74 @@ +// +// RKTableViewCellMappings.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTableViewCellMappings.h" + +@implementation RKTableViewCellMappings + ++ (id)cellMappings { + return [[self new] autorelease]; +} + +- (id)init { + self = [super init]; + if (self) { + _cellMappings = [NSMutableDictionary new]; + } + + return self; +} + +- (void)setCellMapping:(RKTableViewCellMapping*)cellMapping forClass:(Class)objectClass { + if ([_cellMappings objectForKey:objectClass]) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat:@"A tableViewCell mapping has already been registered for objects of type '%@'", NSStringFromClass(objectClass)] + userInfo:nil]; + } + + [_cellMappings setObject:cellMapping forKey:objectClass]; +} + +- (RKTableViewCellMapping*)cellMappingForClass:(Class)objectClass { + // Exact match + RKTableViewCellMapping* cellMapping = [_cellMappings objectForKey:objectClass]; + if (cellMapping) return cellMapping; + + // Subclass match + for (Class cellClass in _cellMappings) { + if ([objectClass isSubclassOfClass:cellClass]) { + return [_cellMappings objectForKey:cellClass]; + } + } + + return nil; +} + +- (RKTableViewCellMapping*)cellMappingForObject:(id)object { + if ([object respondsToSelector:@selector(cellMapping)]) { + // TODO: Trace logging... + // TODO: This needs unit test coverage on the did select row case... + RKTableViewCellMapping* cellMapping = [object cellMapping]; + if (cellMapping) return [object cellMapping]; + } + + return [self cellMappingForClass:[object class]]; +} + +@end diff --git a/Code/UI/UI.h b/Code/UI/UI.h new file mode 100644 index 0000000000..b2de8a3b19 --- /dev/null +++ b/Code/UI/UI.h @@ -0,0 +1,35 @@ +// +// UI.h +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef RestKit_UI_h +#define RestKit_UI_h + +#import "RKTableController.h" +#import "RKTableSection.h" +#import "RKTableItem.h" +#import "RKControlTableItem.h" +#import "RKFetchedResultsTableController.h" +#import "RKForm.h" +#import "RKFormSection.h" +#import "RKRefreshTriggerView.h" +#import "RKObjectManager+RKTableController.h" +#import "UIImage+RKAdditions.h" + +#endif diff --git a/Code/UI/UIImage+RKAdditions.h b/Code/UI/UIImage+RKAdditions.h new file mode 100644 index 0000000000..ee31af3646 --- /dev/null +++ b/Code/UI/UIImage+RKAdditions.h @@ -0,0 +1,42 @@ +// +// UIImage+RKAdditions.h +// RestKit +// +// Created by Blake Watters on 2/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#if TARGET_OS_IPHONE +#import + +/** + Provides useful extensions to the UIImage interface. + + Resolution indepdence helpers borrowed from: + http://atastypixel.com/blog/uiimage-resolution-independence-and-the-iphone-4s-retina-display/ + */ +@interface UIImage (RKAdditions) + +/** + Creates and returns an image object by loading the image data from the file at the specified path + appropriate for the resolution of the device. + + @param path The full or partial path to the file, possibly including an @2x retina image. + @return A new image object for the specified file, or an image for the @2x version of the specified file, + or nil if the method could not initialize the image from the specified file. + */ ++ (UIImage *)imageWithContentsOfResolutionIndependentFile:(NSString *)path; + +/** + Initializes an image object by loading the image data from the file at the specified path + appropriate for the resolution of the device. + + @param path The full or partial path to the file, possibly including an @2x retina image. + @return The initialized image object for the specified file, or for the @2x version of the specified file, + or nil if the method could not initialize the image from the specified file. + */ +- (id)initWithContentsOfResolutionIndependentFile:(NSString *)path; + +@end + +#endif diff --git a/Code/UI/UIImage+RKAdditions.m b/Code/UI/UIImage+RKAdditions.m new file mode 100644 index 0000000000..ef894d6a4c --- /dev/null +++ b/Code/UI/UIImage+RKAdditions.m @@ -0,0 +1,36 @@ +// +// UIImage+RKAdditions.m +// RestKit +// +// Created by Blake Watters on 2/24/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "UIImage+RKAdditions.h" + +#if TARGET_OS_IPHONE + +@implementation UIImage (RKAdditions) + +- (id)initWithContentsOfResolutionIndependentFile:(NSString *)path { + if ( [[[UIDevice currentDevice] systemVersion] intValue] >= 4 && [[UIScreen mainScreen] scale] == 2.0 ) { + NSString *path2x = [[path stringByDeletingLastPathComponent] + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@@2x.%@", + [[path lastPathComponent] stringByDeletingPathExtension], + [path pathExtension]]]; + + if ( [[NSFileManager defaultManager] fileExistsAtPath:path2x] ) { + return [self initWithCGImage:[[UIImage imageWithData:[NSData dataWithContentsOfFile:path2x]] CGImage] scale:2.0 orientation:UIImageOrientationUp]; + } + } + + return [self initWithData:[NSData dataWithContentsOfFile:path]]; +} + ++ (UIImage *)imageWithContentsOfResolutionIndependentFile:(NSString *)path { + return [[[UIImage alloc] initWithContentsOfResolutionIndependentFile:path] autorelease]; +} + +@end + +#endif diff --git a/Code/Network/NSString+MD5.h b/Code/UI/UIView+FindFirstResponder.h similarity index 71% rename from Code/Network/NSString+MD5.h rename to Code/UI/UIView+FindFirstResponder.h index 2d2670c266..e57fad3d7d 100644 --- a/Code/Network/NSString+MD5.h +++ b/Code/UI/UIView+FindFirstResponder.h @@ -1,16 +1,16 @@ // -// NSString+MD5.h +// UIView+FindFirstResponder.h // RestKit // -// Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// +// Created by Blake Watters on 8/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. @@ -18,9 +18,10 @@ // limitations under the License. // +#import -@interface NSString (MD5) +@interface UIView (FindFirstResponder) -- (NSString*)MD5; +- (UIView*)findFirstResponder; @end diff --git a/Code/UI/UIView+FindFirstResponder.m b/Code/UI/UIView+FindFirstResponder.m new file mode 100644 index 0000000000..512b4e6975 --- /dev/null +++ b/Code/UI/UIView+FindFirstResponder.m @@ -0,0 +1,40 @@ +// +// UIView+FindFirstResponder.m +// RestKit +// +// Created by Blake Watters on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "UIView+FindFirstResponder.h" + + +@implementation UIView (FindFirstResponder) + +- (UIView*)findFirstResponder { + if (self.isFirstResponder) { + return self; + } + + for (UIView* subView in self.subviews) { + UIView* firstResponder = [subView findFirstResponder]; + if (firstResponder != nil) { + return firstResponder; + } + } + return nil; +} + +@end 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 6c5dc9f068..333bd29382 100644 --- a/Docs/Object Mapping.md +++ b/Docs/Object Mapping.md @@ -288,7 +288,6 @@ RKObjectMapping* authorMapping = [RKObjectMapping mappingForClass:[Author class] RKObjectMapping* articleMapping = [RKObjectMapping mappingForClass:[Article class]]; [articleMapping mapKeyPath:@"title" toAttribute:@"title"]; [articleMapping mapKeyPath:@"body" toAttribute:@"body"]; -[articleMapping mapKeyPath:@"author" toAttribute:@"author"]; [articleMapping mapKeyPath:@"publication_date" toAttribute:@"publicationDate"]; // Define the relationship mapping @@ -485,7 +484,6 @@ take a look at how this works: ```objc #import -#import RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:@"http://restkit.org"]; RKManagedObjectStore* objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"MyApp.sqlite"]; @@ -739,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; } @@ -860,7 +858,6 @@ RKObjectRelationshipMapping* articleCommentsMapping = [RKObjectRelationshipMappi ### Configuring a Core Data Object Mapping ```objc #import -#import RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:@"http://restkit.org"]; RKManagedObjectStore* objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"MyApp.sqlite"]; 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/RKCatalog.h b/Examples/RKCatalog/App/RKCatalog.h index 96ff1e6472..4ff4ec98c4 100644 --- a/Examples/RKCatalog/App/RKCatalog.h +++ b/Examples/RKCatalog/App/RKCatalog.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -11,4 +11,4 @@ #import // Import the base URL defined in the app delegate -extern NSString* gRKCatalogBaseURL; +extern NSURL* gRKCatalogBaseURL; diff --git a/Examples/RKCatalog/App/RKCatalogAppDelegate.h b/Examples/RKCatalog/App/RKCatalogAppDelegate.h index 77e0915ebe..ebc2ec4a67 100644 --- a/Examples/RKCatalog/App/RKCatalogAppDelegate.h +++ b/Examples/RKCatalog/App/RKCatalogAppDelegate.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKCatalog/App/RKCatalogAppDelegate.m b/Examples/RKCatalog/App/RKCatalogAppDelegate.m index 6e2ef2b9a6..2a327342e0 100644 --- a/Examples/RKCatalog/App/RKCatalogAppDelegate.m +++ b/Examples/RKCatalog/App/RKCatalogAppDelegate.m @@ -3,35 +3,34 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalogAppDelegate.h" #import "RootViewController.h" -NSString* gRKCatalogBaseURL = nil; +NSURL *gRKCatalogBaseURL = nil; @implementation RKCatalogAppDelegate -@synthesize window=_window; -@synthesize navigationController=_navigationController; +@synthesize window; +@synthesize navigationController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. // Add the navigation controller's view to the window and display. self.window.rootViewController = self.navigationController; [self.window makeKeyAndVisible]; - - - // gRKCatalogBaseURL = [@"http://localhost:4567" retain]; - gRKCatalogBaseURL = [@"http://rkcatalog.heroku.com" retain]; - + + + gRKCatalogBaseURL = [[NSURL alloc] initWithString:@"http://rkcatalog.heroku.com"]; + return YES; } - (void)dealloc { - [_window release]; - [_navigationController release]; + [window release]; + [navigationController release]; [super dealloc]; } diff --git a/Examples/RKCatalog/App/RootViewController.h b/Examples/RKCatalog/App/RootViewController.h index 50637c0e4a..598af3bfde 100644 --- a/Examples/RKCatalog/App/RootViewController.h +++ b/Examples/RKCatalog/App/RootViewController.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKCatalog/App/RootViewController.m b/Examples/RKCatalog/App/RootViewController.m index 9c19a551ee..33ee604f67 100644 --- a/Examples/RKCatalog/App/RootViewController.m +++ b/Examples/RKCatalog/App/RootViewController.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -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/App/main.m b/Examples/RKCatalog/App/main.m index de70d34ee6..a9f3903344 100644 --- a/Examples/RKCatalog/App/main.m +++ b/Examples/RKCatalog/App/main.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.h b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.h index 86114476c0..4acf1ccc2e 100644 --- a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.h +++ b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 9/27/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -11,6 +11,7 @@ @interface RKAuthenticationExample : UIViewController +@property (nonatomic, retain) RKRequest *authenticatedRequest; @property (nonatomic, retain) IBOutlet UITextField *URLTextField; @property (nonatomic, retain) IBOutlet UITextField *usernameTextField; @property (nonatomic, retain) IBOutlet UITextField *passwordTextField; diff --git a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m index ff49a3f9f1..cde8d1a74d 100644 --- a/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m +++ b/Examples/RKCatalog/Examples/RKAuthenticationExample/RKAuthenticationExample.m @@ -3,13 +3,14 @@ // RKCatalog // // Created by Blake Watters on 9/27/11. -// Copyright (c) 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKAuthenticationExample.h" @implementation RKAuthenticationExample +@synthesize authenticatedRequest; @synthesize URLTextField; @synthesize usernameTextField; @synthesize passwordTextField; @@ -25,6 +26,14 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil return self; } +- (void)dealloc { + [authenticatedRequest cancel]; + [authenticatedRequest release]; + authenticatedRequest = nil; + + [super dealloc]; +} + /** We are constructing our own RKRequest here rather than working with the client. It is important to remember that RKClient is really just a factory object for instances @@ -32,20 +41,23 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil */ - (void)sendRequest { NSURL *URL = [NSURL URLWithString:[URLTextField text]]; - RKRequest *request = [RKRequest requestWithURL:URL delegate:self]; - request.queue = [RKClient sharedClient].requestQueue; - request.authenticationType = RKRequestAuthenticationTypeHTTP; - request.username = [usernameTextField text]; - request.password = [passwordTextField text]; - [request send]; + RKRequest *newRequest = [RKRequest requestWithURL:URL]; + newRequest.delegate = self; + newRequest.authenticationType = RKRequestAuthenticationTypeHTTP; + newRequest.username = [usernameTextField text]; + newRequest.password = [passwordTextField text]; + + self.authenticatedRequest = newRequest; } - (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error { RKLogError(@"Load of RKRequest %@ failed with error: %@", request, error); + [request release]; } - (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { RKLogCritical(@"Loading of RKRequest %@ completed with status code %d. Response body: %@", request, response.statusCode, [response bodyAsString]); + [request release]; } #pragma mark - UIPickerViewDataSource diff --git a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.h b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.h index 3b600bade3..79e6b768bc 100644 --- a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.h +++ b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m index b8739193d4..2c89b017e3 100644 --- a/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m +++ b/Examples/RKCatalog/Examples/RKBackgroundRequestExample/RKBackgroundRequestExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -21,25 +21,26 @@ - (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]; } - (IBAction)sendRequest { - RKRequest* request = [[RKClient sharedClient] requestWithResourcePath:@"/RKBackgroundRequestExample" delegate:self]; + RKRequest* request = [[RKClient sharedClient] requestWithResourcePath:@"/RKBackgroundRequestExample"]; + request.delegate = self; request.backgroundPolicy = _segmentedControl.selectedSegmentIndex; [request send]; _sendButton.enabled = NO; } - (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.h b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.h index cf75665305..2520f42a96 100644 --- a/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.h +++ b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m index a632c17afe..5bdb5e1173 100644 --- a/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m +++ b/Examples/RKCatalog/Examples/RKCoreDataExample/RKCoreDataExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -34,10 +34,10 @@ @implementation RKCoreDataExample - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { - RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:@"http://restkit.org"]; + 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]; + [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.h b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.h index 17349eb205..09ae2ec400 100644 --- a/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.h +++ b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m index db485a7742..c5e69b9a76 100644 --- a/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m +++ b/Examples/RKCatalog/Examples/RKKeyValueMappingExample/RKKeyValueMappingExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -51,15 +51,15 @@ @implementation RKKeyValueMappingExample - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { - [RKObjectManager objectManagerWithBaseURL:gRKCatalogBaseURL]; + [RKObjectManager managerWithBaseURL:gRKCatalogBaseURL]; } - + return self; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[SimpleAccount class]]; [mapping mapKeyPathsToAttributes: @"id", @"accountID", @@ -69,13 +69,14 @@ - (void)viewDidAppear:(BOOL)animated { @"transactions.@avg.amount", @"averageTransactionAmount", @"transactions.@distinctUnionOfObjects.payee", @"distinctPayees", nil]; - - [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/RKKeyValueMappingExample" objectMapping:mapping delegate:self]; + + [[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.h b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.h index 455e4fb056..fe2654c621 100644 --- a/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.h +++ b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m index 549ce89d14..f7e96d3b47 100644 --- a/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m +++ b/Examples/RKCatalog/Examples/RKParamsExample/RKParamsExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKParamsExample.h" @@ -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.h b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.h index 05f06fab7f..c30ed17e0c 100644 --- a/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.h +++ b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m index 51d9d45acc..c6fdceebf6 100644 --- a/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m +++ b/Examples/RKCatalog/Examples/RKReachabilityExample/RKReachabilityExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -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/Project.h b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.h index 726997fa94..5e28401b66 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.h +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.m b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.m index 7b90f2d68b..e3c8e058c1 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.m +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Project.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "Project.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.h b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.h index 39afb313cd..3bc144eeb5 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.h +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m index b390cc3eeb..d3a006237e 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/RKRelationshipMappingExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -18,21 +18,28 @@ @implementation RKRelationshipMappingExample - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:gRKCatalogBaseURL]; - objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKRelationshipMappingExample.sqlite"]; - - RKManagedObjectMapping* taskMapping = [RKManagedObjectMapping mappingForClass:[Task class]]; + 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]]; + + 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"]; + // NOTE - Project is not backed by Core Data RKObjectMapping* projectMapping = [RKObjectMapping mappingForClass:[Project class]]; [projectMapping mapKeyPath:@"id" toAttribute:@"projectID"]; @@ -41,7 +48,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil [projectMapping mapRelationship:@"tasks" withMapping:taskMapping]; [objectManager.mappingProvider setMapping:projectMapping forKeyPath:@"project"]; } - + return self; } @@ -52,9 +59,9 @@ - (void)dealloc { - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - + self.title = @"Task List"; - + [[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/RKRelationshipMappingExample" delegate:self]; } @@ -86,7 +93,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger return [[[_objects objectAtIndex:indexPath.row] tasks] count]; } } - + return 0; } @@ -94,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) { @@ -111,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; @@ -143,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/RKRelationshipMappingExample/Task.h b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.h index 4b80b6f115..29673d84ba 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.h +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.m b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.m index 104bba111d..3cdc7cfa7e 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.m +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/Task.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "Task.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.h b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.h index 256a9f63f8..7cdf5a0bc4 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.h +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.h @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" diff --git a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.m b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.m index 205b97d4fe..f605e3c3f0 100644 --- a/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.m +++ b/Examples/RKCatalog/Examples/RKRelationshipMappingExample/User.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "User.h" diff --git a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h index 4920261053..5f61107de7 100644 --- a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h +++ b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.h @@ -3,20 +3,17 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKCatalog.h" -@interface RKRequestQueueExample : UIViewController { - UILabel* _statusLabel; - RKRequestQueue* _queue; -} +@interface RKRequestQueueExample : UIViewController -@property (nonatomic, retain) RKRequestQueue* queue; -@property (nonatomic, retain) IBOutlet UILabel* statusLabel; +@property (nonatomic, retain) RKRequestQueue *requestQueue; +@property (nonatomic, retain) IBOutlet UILabel *statusLabel; - (IBAction)sendRequest; - (IBAction)queueRequests; - + @end diff --git a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m index 51208c9ab7..941193a832 100644 --- a/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m +++ b/Examples/RKCatalog/Examples/RKRequestQueueExample/RKRequestQueueExample.m @@ -3,7 +3,7 @@ // RKCatalog // // Created by Blake Watters on 4/21/11. -// Copyright 2011 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -11,29 +11,30 @@ @implementation RKRequestQueueExample -@synthesize queue = _queue; -@synthesize statusLabel = _statusLabel; +@synthesize requestQueue; +@synthesize statusLabel; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle: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; } // We have been dismissed -- clean up any open requests - (void)dealloc { [[RKClient sharedClient].requestQueue cancelRequestsWithDelegate:self]; - [_queue cancelAllRequests]; - [_queue release]; - + [requestQueue cancelAllRequests]; + [requestQueue release]; + requestQueue = nil; + [super dealloc]; } @@ -51,36 +52,44 @@ - (IBAction)sendRequest { } - (IBAction)queueRequests { - RKRequestQueue* queue = [RKRequestQueue new]; + RKRequestQueue *queue = [RKRequestQueue requestQueue]; queue.delegate = self; queue.concurrentRequestsLimit = 1; queue.showsNetworkActivityIndicatorWhenBusy = YES; - + // Queue up 4 requests - [queue addRequest:[[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample" delegate:self]]; - [queue addRequest:[[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample" delegate:self]]; - [queue addRequest:[[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample" delegate:self]]; - [queue addRequest:[[RKClient sharedClient] requestWithResourcePath:@"/RKRequestQueueExample" delegate:self]]; - + 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]; - - // Manage memory for our ad-hoc queue - self.queue = queue; - [queue release]; + 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]]; } - (void)requestQueueDidBeginLoading:(RKRequestQueue *)queue { - _statusLabel.text = [NSString stringWithFormat:@"Queue %@ Began Loading...", queue]; + statusLabel.text = [NSString stringWithFormat:@"Queue %@ Began Loading...", queue]; } - (void)requestQueueDidFinishLoading:(RKRequestQueue *)queue { - _statusLabel.text = [NSString stringWithFormat:@"Queue %@ Finished Loading...", queue]; + statusLabel.text = [NSString stringWithFormat:@"Queue %@ Finished Loading...", queue]; } @end diff --git a/Examples/RKCatalog/RKCatalog.xcodeproj/project.pbxproj b/Examples/RKCatalog/RKCatalog.xcodeproj/project.pbxproj index 6162391452..682a4f5167 100644 --- a/Examples/RKCatalog/RKCatalog.xcodeproj/project.pbxproj +++ b/Examples/RKCatalog/RKCatalog.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 258F33F51432CB11001EEEFC /* RKAuthenticationExample.xib in Resources */ = {isa = PBXBuildFile; fileRef = 258F33F31432CB11001EEEFC /* RKAuthenticationExample.xib */; }; 25A34207147C4C540009758D /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A341FF147C4C300009758D /* libRestKit.a */; }; 25A3421D147D87800009758D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A3421C147D87800009758D /* Security.framework */; }; + 25BE938114F96CAD008BC1C0 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25BE938014F96CAD008BC1C0 /* QuartzCore.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,13 @@ remoteGlobalIDString = 25160D1514564E810060A5C5; remoteInfo = RestKit; }; + 257AB9DD150EB52000CCAA76 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2501DE8113607B74003DE9E4 /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 25EC1AFF14F8078100C3CF3F; + remoteInfo = RestKitResources; + }; 25A341FE147C4C300009758D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2501DE8113607B74003DE9E4 /* RestKit.xcodeproj */; @@ -141,6 +149,7 @@ 258F33F21432CB11001EEEFC /* RKAuthenticationExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKAuthenticationExample.m; path = RKAuthenticationExample/RKAuthenticationExample.m; sourceTree = ""; }; 258F33F31432CB11001EEEFC /* RKAuthenticationExample.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = RKAuthenticationExample.xib; path = RKAuthenticationExample/RKAuthenticationExample.xib; sourceTree = ""; }; 25A3421C147D87800009758D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 25BE938014F96CAD008BC1C0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -148,6 +157,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 25BE938114F96CAD008BC1C0 /* QuartzCore.framework in Frameworks */, 25A3421D147D87800009758D /* Security.framework in Frameworks */, 25A34207147C4C540009758D /* libRestKit.a in Frameworks */, 258BAA6513609F6D00ED0614 /* MobileCoreServices.framework in Frameworks */, @@ -185,6 +195,7 @@ 2501DE5B13607B67003DE9E4 /* Frameworks */ = { isa = PBXGroup; children = ( + 25BE938014F96CAD008BC1C0 /* QuartzCore.framework */, 2501DE8113607B74003DE9E4 /* RestKit.xcodeproj */, 25A3421C147D87800009758D /* Security.framework */, 258BAA6413609F6D00ED0614 /* MobileCoreServices.framework */, @@ -206,6 +217,7 @@ 25A34201147C4C300009758D /* RestKitTests.octest */, 25A34203147C4C300009758D /* RestKit.framework */, 25A34205147C4C300009758D /* RestKitFrameworkTests.octest */, + 257AB9DE150EB52000CCAA76 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -386,6 +398,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 257AB9DE150EB52000CCAA76 /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = 257AB9DD150EB52000CCAA76 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 25A341FF147C4C300009758D /* libRestKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -550,7 +569,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "App/RKCatalog-Prefix.pch"; - HEADER_SEARCH_PATHS = ""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "App/RKCatalog-Info.plist"; LIBRARY_SEARCH_PATHS = "\"$(SRCROOT)\""; OTHER_LDFLAGS = "-ObjC"; @@ -566,7 +585,7 @@ COPY_PHASE_STRIP = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "App/RKCatalog-Prefix.pch"; - HEADER_SEARCH_PATHS = ""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "App/RKCatalog-Info.plist"; LIBRARY_SEARCH_PATHS = "\"$(SRCROOT)\""; OTHER_LDFLAGS = "-ObjC"; diff --git a/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj b/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj index 429f6afcc6..73e964d662 100644 --- a/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj +++ b/Examples/RKMacOSX/RKMacOSX.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 25A8BA7A14F9458C005C7314 /* RestKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2534806B14F941A900565CED /* RestKit.framework */; }; + 250DF24F14C67F560001DEFA /* RestKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 250DF24B14C67E9A0001DEFA /* RestKit.framework */; }; 25D63919135184CE000879B1 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25D63918135184CE000879B1 /* Cocoa.framework */; }; 25D63923135184CE000879B1 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25D63921135184CE000879B1 /* InfoPlist.strings */; }; 25D63926135184CE000879B1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 25D63925135184CE000879B1 /* main.m */; }; @@ -20,40 +20,47 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 2534806614F941A900565CED /* PBXContainerItemProxy */ = { + 250DF24614C67E9A0001DEFA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 25160D1614564E810060A5C5; remoteInfo = RestKit; }; - 2534806814F941A900565CED /* PBXContainerItemProxy */ = { + 250DF24814C67E9A0001DEFA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 25160D2614564E820060A5C5; remoteInfo = RestKitTests; }; - 2534806A14F941A900565CED /* PBXContainerItemProxy */ = { + 250DF24A14C67E9A0001DEFA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 25160E62145651060060A5C5; remoteInfo = RestKitFramework; }; - 2534806C14F941A900565CED /* PBXContainerItemProxy */ = { + 250DF24C14C67E9A0001DEFA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 25160E78145651060060A5C5; remoteInfo = RestKitFrameworkTests; }; - 25D6397013518504000879B1 /* PBXContainerItemProxy */ = { + 250DF25014C67F630001DEFA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; proxyType = 1; - remoteGlobalIDString = 25160D1514564E810060A5C5; - remoteInfo = RestKit; + remoteGlobalIDString = 25160E61145651060060A5C5; + remoteInfo = RestKitFramework; + }; + 256780AF152D1771000725F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 25D63938135184F0000879B1 /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 259C301615128079003066A2; + remoteInfo = RestKitResources; }; /* End PBXContainerItemProxy section */ @@ -72,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; }; @@ -88,7 +89,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 25A8BA7A14F9458C005C7314 /* RestKit.framework in Frameworks */, + 250DF24F14C67F560001DEFA /* RestKit.framework in Frameworks */, 25D63983135185B6000879B1 /* SystemConfiguration.framework in Frameworks */, 25D639811351858A000879B1 /* AppKit.framework in Frameworks */, 25D6397F13518574000879B1 /* CoreData.framework in Frameworks */, @@ -165,10 +166,11 @@ 25D63939135184F0000879B1 /* Products */ = { isa = PBXGroup; children = ( - 2534806714F941A900565CED /* libRestKit.a */, - 2534806914F941A900565CED /* RestKitTests.octest */, - 2534806B14F941A900565CED /* RestKit.framework */, - 2534806D14F941A900565CED /* RestKitFrameworkTests.octest */, + 250DF24714C67E9A0001DEFA /* libRestKit.a */, + 250DF24914C67E9A0001DEFA /* RestKitTests.octest */, + 250DF24B14C67E9A0001DEFA /* RestKit.framework */, + 250DF24D14C67E9A0001DEFA /* RestKitFrameworkTests.octest */, + 256780B0152D1771000725F5 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -177,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 = ""; @@ -201,7 +197,7 @@ buildRules = ( ); dependencies = ( - 25D6397113518504000879B1 /* PBXTargetDependency */, + 250DF25114C67F630001DEFA /* PBXTargetDependency */, ); name = RKMacOSX; productName = RKMacOSX; @@ -214,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"; @@ -240,32 +236,39 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 2534806714F941A900565CED /* libRestKit.a */ = { + 250DF24714C67E9A0001DEFA /* libRestKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRestKit.a; - remoteRef = 2534806614F941A900565CED /* PBXContainerItemProxy */; + remoteRef = 250DF24614C67E9A0001DEFA /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 2534806914F941A900565CED /* RestKitTests.octest */ = { + 250DF24914C67E9A0001DEFA /* RestKitTests.octest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; path = RestKitTests.octest; - remoteRef = 2534806814F941A900565CED /* PBXContainerItemProxy */; + remoteRef = 250DF24814C67E9A0001DEFA /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 2534806B14F941A900565CED /* RestKit.framework */ = { + 250DF24B14C67E9A0001DEFA /* RestKit.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = RestKit.framework; - remoteRef = 2534806A14F941A900565CED /* PBXContainerItemProxy */; + remoteRef = 250DF24A14C67E9A0001DEFA /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 2534806D14F941A900565CED /* RestKitFrameworkTests.octest */ = { + 250DF24D14C67E9A0001DEFA /* RestKitFrameworkTests.octest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; path = RestKitFrameworkTests.octest; - remoteRef = 2534806C14F941A900565CED /* PBXContainerItemProxy */; + remoteRef = 250DF24C14C67E9A0001DEFA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 256780B0152D1771000725F5 /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = 256780AF152D1771000725F5 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ @@ -296,10 +299,10 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 25D6397113518504000879B1 /* PBXTargetDependency */ = { + 250DF25114C67F630001DEFA /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = RestKit; - targetProxy = 25D6397013518504000879B1 /* PBXContainerItemProxy */; + name = RestKitFramework; + targetProxy = 250DF25014C67F630001DEFA /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -334,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; @@ -353,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; @@ -374,7 +377,7 @@ GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RKMacOSX/RKMacOSX-Prefix.pch"; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "RKMacOSX/RKMacOSX-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -395,7 +398,7 @@ GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "RKMacOSX/RKMacOSX-Prefix.pch"; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "RKMacOSX/RKMacOSX-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", diff --git a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.h b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.h index f223d20005..9c00b1d3fa 100644 --- a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.h +++ b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.h @@ -3,17 +3,15 @@ // RKMacOSX // // Created by Blake Watters on 4/10/11. -// Copyright 2011 __MyCompanyName__. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import #import -@interface RKMacOSXAppDelegate : NSObject { -@private - NSWindow *window; -} +@interface RKMacOSXAppDelegate : NSObject +@property (nonatomic, retain) RKClient *client; @property (assign) IBOutlet NSWindow *window; @end diff --git a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m index 111b68f69a..1f44acafab 100644 --- a/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m +++ b/Examples/RKMacOSX/RKMacOSX/RKMacOSXAppDelegate.m @@ -3,20 +3,21 @@ // RKMacOSX // // Created by Blake Watters on 4/10/11. -// Copyright 2011 __MyCompanyName__. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKMacOSXAppDelegate.h" @implementation RKMacOSXAppDelegate -@synthesize window; +@synthesize client = _client; +@synthesize window = _window; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Initialize RestKit - RKClient* client = [RKClient clientWithBaseURL:@"http://twitter.com"]; - [client get:@"/status/user_timeline/twotoasters.json" delegate:self]; + self.client = [RKClient clientWithBaseURL:[RKURL URLWithBaseURLString:@"http://twitter.com"]]; + [self.client get:@"/status/user_timeline/RestKit.json" delegate:self]; } - (void)request:(RKRequest*)request didLoadResponse:(RKResponse *)response { diff --git a/Examples/RKMacOSX/RKMacOSX/main.m b/Examples/RKMacOSX/RKMacOSX/main.m index 990cc8597c..9693d475ac 100644 --- a/Examples/RKMacOSX/RKMacOSX/main.m +++ b/Examples/RKMacOSX/RKMacOSX/main.m @@ -3,7 +3,7 @@ // RKMacOSX // // Created by Blake Watters on 4/10/11. -// Copyright 2011 __MyCompanyName__. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKTwitter/Classes/RKTStatus.h b/Examples/RKTwitter/Classes/RKTStatus.h index 637f3298ae..a3c23afef8 100644 --- a/Examples/RKTwitter/Classes/RKTStatus.h +++ b/Examples/RKTwitter/Classes/RKTStatus.h @@ -3,19 +3,19 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #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 02e6a07662..0bbf464549 100644 --- a/Examples/RKTwitter/Classes/RKTStatus.m +++ b/Examples/RKTwitter/Classes/RKTStatus.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKTStatus.h" @@ -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 d90e3d8e41..20a6508b61 100644 --- a/Examples/RKTwitter/Classes/RKTUser.h +++ b/Examples/RKTwitter/Classes/RKTUser.h @@ -3,13 +3,13 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // @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 2dca69d5b4..aabd1d5f3b 100644 --- a/Examples/RKTwitter/Classes/RKTUser.m +++ b/Examples/RKTwitter/Classes/RKTUser.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKTUser.h" @@ -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.h b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.h index 3379b345de..2afba3adf4 100644 --- a/Examples/RKTwitter/Classes/RKTwitterAppDelegate.h +++ b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.h @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m index 4d2249b772..7fa71dbe53 100644 --- a/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m +++ b/Examples/RKTwitter/Classes/RKTwitterAppDelegate.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -19,47 +19,48 @@ @implementation RKTwitterAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { RKLogConfigureByName("RestKit/Network*", RKLogLevelTrace); + RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); + // Initialize RestKit - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:@"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", - @"text", @"text", - @"url", @"urlString", - @"in_reply_to_screen_name", @"inReplyToScreenName", - @"favorited", @"isFavorited", - nil]; + @"created_at", @"createdAt", + @"text", @"text", + @"url", @"urlString", + @"in_reply_to_screen_name", @"inReplyToScreenName", + @"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 setMapping:userMapping forKeyPath:@"user"]; - [objectManager.mappingProvider setMapping:statusMapping forKeyPath:@"status"]; - - // Uncomment this to use XML, comment it to use JSON -// objectManager.acceptMIMEType = RKMIMETypeXML; -// [objectManager.mappingProvider setMapping:statusMapping forKeyPath:@"statuses.status"]; - + + // 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 ac6a10629a..19abdbbf83 100644 --- a/Examples/RKTwitter/Classes/RKTwitterViewController.h +++ b/Examples/RKTwitter/Classes/RKTwitterViewController.h @@ -3,15 +3,15 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import #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 35c6e208ee..6e985a9b92 100644 --- a/Examples/RKTwitter/Classes/RKTwitterViewController.m +++ b/Examples/RKTwitter/Classes/RKTwitterViewController.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright RestKit 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKTwitterViewController.h" @@ -16,45 +16,39 @@ - (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 = @"http://www.twitter.com"; - [objectManager loadObjectsAtResourcePath:@"/status/user_timeline/RestKit" delegate:self block:^(RKObjectLoader* loader) { - // Twitter returns statuses as a naked array in JSON, so we instruct the loader - // to user the appropriate object mapping - if ([objectManager.acceptMIMEType isEqualToString:RKMIMETypeJSON]) { - loader.objectMapping = [objectManager.mappingProvider objectMappingForClass:[RKTStatus class]]; - } - }]; + objectManager.client.baseURL = [RKURL URLWithString:@"http://www.twitter.com"]; + [objectManager loadObjectsAtResourcePath:@"/status/user_timeline/RestKit" delegate:self]; } - (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]; } @@ -65,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/RKTwitter.xcodeproj/project.pbxproj b/Examples/RKTwitter/RKTwitter.xcodeproj/project.pbxproj index 9ddf980fb3..d0ec42d783 100755 --- a/Examples/RKTwitter/RKTwitter.xcodeproj/project.pbxproj +++ b/Examples/RKTwitter/RKTwitter.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 2538E811123419CA00ACB5D7 /* RKTUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538E810123419CA00ACB5D7 /* RKTUser.m */; }; 2538E814123419EC00ACB5D7 /* RKTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538E813123419EC00ACB5D7 /* RKTStatus.m */; }; 2538E8671234250100ACB5D7 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2538E8661234250100ACB5D7 /* SystemConfiguration.framework */; }; + 25BE936614F96729008BC1C0 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25BE936514F96729008BC1C0 /* QuartzCore.framework */; }; 288765A50DF7441C002DB57D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288765A40DF7441C002DB57D /* CoreGraphics.framework */; }; 28D7ACF80DDB3853001CB0EB /* RKTwitterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 28D7ACF70DDB3853001CB0EB /* RKTwitterViewController.m */; }; 3F02F592131D683A004E1F54 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F02F591131D683A004E1F54 /* libxml2.dylib */; }; @@ -66,6 +67,13 @@ remoteGlobalIDString = 25160E78145651060060A5C5; remoteInfo = RestKitFrameworkTests; }; + D3FAFBE1151B815C00468702 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 250AC48A1358C79C006F084F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 259C301615128079003066A2; + remoteInfo = RestKitResources; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -82,6 +90,7 @@ 2538E813123419EC00ACB5D7 /* RKTStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTStatus.m; sourceTree = ""; }; 2538E864123424F000ACB5D7 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 2538E8661234250100ACB5D7 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 25BE936514F96729008BC1C0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 288765A40DF7441C002DB57D /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 28D7ACF60DDB3853001CB0EB /* RKTwitterViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTwitterViewController.h; sourceTree = ""; }; 28D7ACF70DDB3853001CB0EB /* RKTwitterViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTwitterViewController.m; sourceTree = ""; }; @@ -104,6 +113,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 25BE936614F96729008BC1C0 /* QuartzCore.framework in Frameworks */, 250CA69C147D8FFD0047D347 /* CoreData.framework in Frameworks */, 250CA69B147D8FD30047D347 /* libRestKit.a in Frameworks */, 250CA69A147D8FCC0047D347 /* Security.framework in Frameworks */, @@ -150,6 +160,7 @@ 25160FB51456E8A30060A5C5 /* RestKitTests.octest */, 25160FB71456E8A30060A5C5 /* RestKit.framework */, 25160FB91456E8A30060A5C5 /* RestKitFrameworkTests.octest */, + D3FAFBE2151B815C00468702 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -193,6 +204,7 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + 25BE936514F96729008BC1C0 /* QuartzCore.framework */, 250CA699147D8FCC0047D347 /* Security.framework */, 3F02F591131D683A004E1F54 /* libxml2.dylib */, 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */, @@ -233,7 +245,7 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0420; + LastUpgradeCheck = 0430; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "RKTwitter" */; compatibilityVersion = "Xcode 3.2"; @@ -289,6 +301,13 @@ remoteRef = 25160FB81456E8A30060A5C5 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + D3FAFBE2151B815C00468702 /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = D3FAFBE1151B815C00468702 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -341,7 +360,7 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RKTwitter; @@ -355,7 +374,7 @@ COPY_PHASE_STRIP = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RKTwitter; diff --git a/Examples/RKTwitter/main.m b/Examples/RKTwitter/main.m index 0fe5b0b467..68a6317613 100644 --- a/Examples/RKTwitter/main.m +++ b/Examples/RKTwitter/main.m @@ -3,13 +3,13 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #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/RKTStatus.h b/Examples/RKTwitterCoreData/Classes/RKTStatus.h index 65db76fd3d..caa1370d56 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTStatus.h +++ b/Examples/RKTwitterCoreData/Classes/RKTStatus.h @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKTwitterCoreData/Classes/RKTStatus.m b/Examples/RKTwitterCoreData/Classes/RKTStatus.m index 17f3d2f3d9..7cd8ad09c4 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTStatus.m +++ b/Examples/RKTwitterCoreData/Classes/RKTStatus.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright 2010 Two Toasters. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKTStatus.h" diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.h b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.h index 3379b345de..2afba3adf4 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.h +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.h @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import diff --git a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m index ac3dea1a4b..bd2802a451 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterAppDelegate.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -19,11 +19,11 @@ @implementation RKTwitterAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize RestKit - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:@"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,47 +34,46 @@ - (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 -- there is no backing model class! */ - RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKTUser"]; + RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKTUser" inManagedObjectStore:objectManager.objectStore]; userMapping.primaryKeyAttribute = @"userID"; [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. Twitter status objects will be mapped onto RKTStatus instances. */ - RKManagedObjectMapping* statusMapping = [RKManagedObjectMapping mappingForClass:[RKTStatus class]]; + RKManagedObjectMapping* statusMapping = [RKManagedObjectMapping mappingForClass:[RKTStatus class] inManagedObjectStore:objectManager.objectStore]; statusMapping.primaryKeyAttribute = @"statusID"; [statusMapping mapKeyPathsToAttributes:@"id", @"statusID", @"created_at", @"createdAt", @"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 setMapping:userMapping forKeyPath:@"user"]; - [objectManager.mappingProvider setMapping:statusMapping forKeyPath:@"status"]; - + [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 @@ -85,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 00c04becff..909f46526c 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.h @@ -3,15 +3,15 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import #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 794655f5dd..e4eb73dd2f 100644 --- a/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m +++ b/Examples/RKTwitterCoreData/Classes/RKTwitterViewController.m @@ -3,7 +3,7 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import "RKTwitterViewController.h" @@ -13,112 +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 block:^(RKObjectLoader* loader) { - // Twitter returns statuses as a naked array in JSON, so we instruct the loader - // to user the appropriate object mapping - loader.objectMapping = [objectManager.mappingProvider objectMappingForClass:[RKTStatus class]]; - }]; + [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 d929608937..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 */; }; @@ -49,6 +52,7 @@ 3FB46641131D78A400E37C51 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FB46640131D78A400E37C51 /* libxml2.dylib */; }; 8452D3F9128244D90069F4A9 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8452D3F8128244D90069F4A9 /* CFNetwork.framework */; }; 8452D3FD128244E60069F4A9 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8452D3FC128244E60069F4A9 /* MobileCoreServices.framework */; }; + CD39C14E14EEED8700E84874 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD39C14D14EEED8700E84874 /* QuartzCore.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -80,6 +84,13 @@ remoteGlobalIDString = 25160E78145651060060A5C5; remoteInfo = RestKitFrameworkTests; }; + 256780DD152D1A92000725F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2538E7FF123417E500ACB5D7 /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 259C301615128079003066A2; + remoteInfo = RestKitResources; + }; 25F2A1771322D59400A33DE4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2538E7FF123417E500ACB5D7 /* RestKit.xcodeproj */; @@ -128,6 +139,7 @@ 8452D3F8128244D90069F4A9 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 8452D3FC128244E60069F4A9 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 8D1107310486CEB800E47090 /* RKTwitter-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "RKTwitter-Info.plist"; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; + CD39C14D14EEED8700E84874 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -135,6 +147,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CD39C14E14EEED8700E84874 /* QuartzCore.framework in Frameworks */, 250CA6D0147D90C50047D347 /* Security.framework in Frameworks */, 250CA6CE147D90BA0047D347 /* libRestKit.a in Frameworks */, 3FB46641131D78A400E37C51 /* libxml2.dylib in Frameworks */, @@ -152,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 */, @@ -194,6 +210,7 @@ 250CA6C8147D90A50047D347 /* RestKitTests.octest */, 250CA6CA147D90A50047D347 /* RestKit.framework */, 250CA6CC147D90A50047D347 /* RestKitFrameworkTests.octest */, + 256780DE152D1A92000725F5 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -241,6 +258,7 @@ 29B97323FDCFA39411CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + CD39C14D14EEED8700E84874 /* QuartzCore.framework */, 250CA6CF147D90C50047D347 /* Security.framework */, 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */, 1D30AB110D05D00D00671497 /* Foundation.framework */, @@ -356,6 +374,13 @@ remoteRef = 250CA6CB147D90A50047D347 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 256780DE152D1A92000725F5 /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = 256780DD152D1A92000725F5 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -440,7 +465,7 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/../../Headers\""; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RKTwitterCoreData; @@ -454,7 +479,7 @@ COPY_PHASE_STRIP = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = RKTwitter_Prefix.pch; - HEADER_SEARCH_PATHS = "\"$(SOURCE_ROOT)/../../Build\""; + HEADER_SEARCH_PATHS = "${TARGET_BUILD_DIR}/../../Headers"; INFOPLIST_FILE = "Resources/RKTwitter-Info.plist"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RKTwitterCoreData; @@ -472,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"; @@ -487,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 0fe5b0b467..68a6317613 100644 --- a/Examples/RKTwitterCoreData/main.m +++ b/Examples/RKTwitterCoreData/main.m @@ -3,13 +3,13 @@ // RKTwitter // // Created by Blake Watters on 9/5/10. -// Copyright Two Toasters 2010. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #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.xcodeproj/project.pbxproj b/Examples/RestKit CLI/RestKit CLI.xcodeproj/project.pbxproj index 959421cbc8..6747fa621c 100644 --- a/Examples/RestKit CLI/RestKit CLI.xcodeproj/project.pbxproj +++ b/Examples/RestKit CLI/RestKit CLI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 250DF27B14C684510001DEFA /* RestKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A3422B147D885B0009758D /* RestKit.framework */; }; 2516F0FC144A8577004631A1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2516F0FB144A8577004631A1 /* Foundation.framework */; }; 2516F0FF144A8577004631A1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516F0FE144A8577004631A1 /* main.m */; }; 2516F103144A8577004631A1 /* RestKit_CLI.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = 2516F102144A8577004631A1 /* RestKit_CLI.1 */; }; @@ -17,6 +18,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 25678093152D124D000725F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2516F10D144A8841004631A1 /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 259C301615128079003066A2; + remoteInfo = RestKitResources; + }; 25A34226147D885B0009758D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2516F10D144A8841004631A1 /* RestKit.xcodeproj */; @@ -85,6 +93,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 250DF27B14C684510001DEFA /* RestKit.framework in Frameworks */, 25A34230147D88630009758D /* Security.framework in Frameworks */, 2516F162144A8AED004631A1 /* CoreServices.framework in Frameworks */, 2516F160144A895E004631A1 /* libxml2.dylib in Frameworks */, @@ -151,6 +160,7 @@ 25A34229147D885B0009758D /* RestKitTests.octest */, 25A3422B147D885B0009758D /* RestKit.framework */, 25A3422D147D885B0009758D /* RestKitFrameworkTests.octest */, + 25678094152D124D000725F5 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -209,6 +219,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 25678094152D124D000725F5 /* RestKitResources.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitResources.bundle; + remoteRef = 25678093152D124D000725F5 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 25A34227147D885B0009758D /* libRestKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/RestKit CLI/RestKit CLI/main.m b/Examples/RestKit CLI/RestKit CLI/main.m index 56e7c0be24..b3ea0cf162 100644 --- a/Examples/RestKit CLI/RestKit CLI/main.m +++ b/Examples/RestKit CLI/RestKit CLI/main.m @@ -3,7 +3,7 @@ // RestKit CLI // // Created by Blake Watters on 10/15/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // #import @@ -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 85d0f9eea1..c61e70c5a1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,20 @@ source "http://rubygems.org" -gem "rake" -gem "bundler", "~> 1.0.0" -gem "sinatra", ">= 1.2.6" -gem "thin", ">= 1.2.11" -gem "ruby-debug19" -gem "sinatra-reloader" \ No newline at end of file +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 f535821cf8..1a70d42182 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,17 +1,88 @@ +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: 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: archive-tar-minitar (0.5.2) - backports (2.3.0) - columnize (0.3.4) - daemons (1.1.4) + 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) - monkey-lib (0.5.4) - backports - rack (1.2.1) - rake (0.8.7) + 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) @@ -22,32 +93,26 @@ GEM ruby-debug-base19 (>= 0.11.19) ruby_core_source (0.1.5) archive-tar-minitar (>= 0.5.2) - sinatra (1.2.6) - rack (~> 1.1) - tilt (>= 1.2.2, < 2.0) - sinatra-advanced-routes (0.5.1) - monkey-lib (~> 0.5.0) - sinatra (~> 1.0) - sinatra-sugar (~> 0.5.0) - sinatra-reloader (0.5.0) - sinatra (~> 1.0) - sinatra-advanced-routes (~> 0.5.0) - sinatra-sugar (0.5.1) - monkey-lib (~> 0.5.0) - sinatra (~> 1.0) - thin (1.2.11) + thin (1.3.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) - tilt (1.3.2) + tilt (1.3.3) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.0.0) - rake + bundler (~> 1.1.0) + faker (= 1.0.1) + mongo + rack-oauth2-server! + rack-test + rake (~> 0.9.0) + restkit! + rspec ruby-debug19 - sinatra (>= 1.2.6) - sinatra-reloader - thin (>= 1.2.11) + simple_oauth! + sinatra! + thin (~> 1.3.1) + xcoder! diff --git a/README.md b/README.md index 9e7611ed8a..b417b1e3a0 100644 --- a/README.md +++ b/README.md @@ -74,16 +74,18 @@ button and confirm that your "Build Location" is the "Derived Data Location". 1. Add Git submodule to your project: `git submodule add git://github.com/RestKit/RestKit.git RestKit` 1. Add cross-project reference by dragging **RestKit.xcodeproj** to your project 1. Open build settings editor for your project +1. Add the following **Header Search Paths** (including the quotes): `"$(BUILT_PRODUCTS_DIR)/../../Headers"` 1. Add **Other Linker Flags** for `-ObjC -all_load` 1. Open target settings editor for the target you want to link RestKit into 1. Add direct dependency on the **RestKit** aggregate target 1. Link against required frameworks: - 1. **CFNetwork.framework** + 1. **CFNetwork.framework** on iOS 1. **CoreData.framework** 1. **Security.framework** 1. **MobileCoreServices.framework** on iOS or **CoreServices.framework** on OS X 1. **SystemConfiguration.framework** 1. **libxml2.dylib** + 1. **QuartzCore.framework** on iOS 1. Link against RestKit: 1. **libRestKit.a** on iOS 1. **RestKit.framework** on OS X @@ -100,6 +102,9 @@ Community Resources A Google Group (high traffic) for development discussions and user support is available at: [http://groups.google.com/group/restkit](http://groups.google.com/group/restkit) +The preferred venue for discussing bugs and feature requests is on Github Issues. The mailing list support traffic can be overwhelming +for our small development team. Please file all bug reports and feature requests at: + For users interested in low traffic updates about the library, an announcements list is also available: [http://groups.google.com/group/restkit-announce](http://groups.google.com/group/restkit-announce) diff --git a/Rakefile b/Rakefile index a08748c51d..233efb0c88 100644 --- a/Rakefile +++ b/Rakefile @@ -1,13 +1,99 @@ require 'rubygems' +require 'bundler/setup' +require 'xcoder' +require 'restkit/rake' +require 'ruby-debug' -namespace :spec do - desc "Run the RestKit spec server" - task :server do - server_path = File.dirname(__FILE__) + '/Specs/Server/server.rb' - system("ruby #{server_path}") +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 + 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,14 +114,11 @@ def run(command, min_exit_status = 0) return $?.exitstatus end -task :default => 'spec: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") run("xcodebuild -workspace RestKit.xcodeproj/project.xcworkspace -scheme RestKit -sdk iphoneos clean build") run("xcodebuild -workspace RestKit.xcodeproj/project.xcworkspace -scheme RestKit -sdk macosx10.6 clean build") - run("xcodebuild -workspace RestKit.xcodeproj/project.xcworkspace -scheme RestKitThree20 -sdk iphoneos clean build") run("xcodebuild -workspace Examples/RKCatalog/RKCatalog.xcodeproj/project.xcworkspace -scheme RKCatalog -sdk iphoneos clean build") end @@ -62,8 +145,8 @@ namespace :docs do exit(exitstatus) else puts "!! appledoc generation failed with a fatal error" - exit(exitstatus) end + exit(exitstatus) end desc "Generate & install a docset into Xcode from the current sources" @@ -72,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" << @@ -82,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 Specs server listening on port 4567. Run `rake uispec: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 @@ -144,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 - puts "Project stated validated successfully. Proceed with merge." +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/RestKitResources-Info.plist b/Resources/PLISTs/RestKitResources-Info.plist new file mode 100644 index 0000000000..41f25e77dc --- /dev/null +++ b/Resources/PLISTs/RestKitResources-Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + org.restkit.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + CFPlugInDynamicRegisterFunction + + CFPlugInDynamicRegistration + NO + CFPlugInFactories + + 00000000-0000-0000-0000-000000000000 + MyFactoryFunction + + CFPlugInTypes + + 00000000-0000-0000-0000-000000000000 + + 00000000-0000-0000-0000-000000000000 + + + CFPlugInUnloadFunction + + NSHumanReadableCopyright + Copyright © 2012 RestKit. All rights reserved. + + 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/Resources/RKRefreshTriggerViewAssets/blackArrow.png b/Resources/RKRefreshTriggerViewAssets/blackArrow.png new file mode 100644 index 0000000000..6d2ffbc3a2 Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/blackArrow.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/blackArrow@2x.png b/Resources/RKRefreshTriggerViewAssets/blackArrow@2x.png new file mode 100644 index 0000000000..ec4ec0e007 Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/blackArrow@2x.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/blueArrow.png b/Resources/RKRefreshTriggerViewAssets/blueArrow.png new file mode 100644 index 0000000000..d6240d4a22 Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/blueArrow.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/blueArrow@2x.png b/Resources/RKRefreshTriggerViewAssets/blueArrow@2x.png new file mode 100644 index 0000000000..a6a7e55c9b Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/blueArrow@2x.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/grayArrow.png b/Resources/RKRefreshTriggerViewAssets/grayArrow.png new file mode 100644 index 0000000000..d69e9d94e6 Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/grayArrow.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/grayArrow@2x.png b/Resources/RKRefreshTriggerViewAssets/grayArrow@2x.png new file mode 100644 index 0000000000..9d42357cf6 Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/grayArrow@2x.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/whiteArrow.png b/Resources/RKRefreshTriggerViewAssets/whiteArrow.png new file mode 100644 index 0000000000..4bf569025c Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/whiteArrow.png differ diff --git a/Resources/RKRefreshTriggerViewAssets/whiteArrow@2x.png b/Resources/RKRefreshTriggerViewAssets/whiteArrow@2x.png new file mode 100644 index 0000000000..70569746db Binary files /dev/null and b/Resources/RKRefreshTriggerViewAssets/whiteArrow@2x.png differ diff --git a/Resources/RestKitCoreData.xcdatamodeld/.xccurrentversion b/Resources/RestKitCoreData.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000000..ee8fe3403b --- /dev/null +++ b/Resources/RestKitCoreData.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + RestKitCoreData.xcdatamodel + + diff --git a/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/elements b/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/elements new file mode 100644 index 0000000000..bbfabdd438 Binary files /dev/null and b/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/elements differ diff --git a/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/layout b/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/layout new file mode 100644 index 0000000000..5973f1318c Binary files /dev/null and b/Resources/RestKitCoreData.xcdatamodeld/RestKitCoreData.xcdatamodel/layout differ diff --git a/Resources/RestKitResources-Prefix.pch b/Resources/RestKitResources-Prefix.pch new file mode 100644 index 0000000000..24962c8fb5 --- /dev/null +++ b/Resources/RestKitResources-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'RestKitResources' target in the 'RestKitResources' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/Specs/Runner/OCMock/OCMock/en.lproj/InfoPlist.strings b/Resources/en.lproj/InfoPlist.strings similarity index 97% rename from Specs/Runner/OCMock/OCMock/en.lproj/InfoPlist.strings rename to Resources/en.lproj/InfoPlist.strings index 477b28ff8f..b92732c79e 100644 --- a/Specs/Runner/OCMock/OCMock/en.lproj/InfoPlist.strings +++ b/Resources/en.lproj/InfoPlist.strings @@ -1,2 +1 @@ /* Localized versions of Info.plist keys */ - diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 5c105f2873..9f55bfc2bc 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -7,9 +7,41 @@ 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"; }; }; + 25055B8714EEF32A00B9C4DD /* RKMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8114EEF32A00B9C4DD /* RKMappingTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 25055B8814EEF32A00B9C4DD /* RKTestFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8214EEF32A00B9C4DD /* RKTestFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25055B8914EEF32A00B9C4DD /* RKTestFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8214EEF32A00B9C4DD /* RKTestFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25055B8A14EEF32A00B9C4DD /* RKTestFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8314EEF32A00B9C4DD /* RKTestFactory.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 25055B8B14EEF32A00B9C4DD /* RKTestFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8314EEF32A00B9C4DD /* RKTestFactory.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 25055B8F14EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8D14EEF40000B9C4DD /* RKMappingTestExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25055B9014EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 25055B8D14EEF40000B9C4DD /* RKMappingTestExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25055B9114EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 25055B9214EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 25055B9314EEFEC800B9C4DD /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160D1614564E810060A5C5 /* libRestKit.a */; }; + 25079C6F151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25079C6D151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25079C70151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25079C6D151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25079C71151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25079C6E151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m */; }; + 25079C72151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25079C6E151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m */; }; + 25079C76151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */; }; + 25079C77151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */; }; + 250B849E152B6F63002581F9 /* RKObjectMappingProvider+CoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 73DA8E1B14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.m */; }; 250CA67D147D8E8B0047D347 /* OCHamcrest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA67B147D8E800047D347 /* OCHamcrest.framework */; }; 250CA67E147D8E8F0047D347 /* OCHamcrestIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 250CA67C147D8E800047D347 /* OCHamcrestIOS.framework */; }; 250CA680147D8F050047D347 /* OCHamcrest.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 250CA67B147D8E800047D347 /* OCHamcrest.framework */; }; + 250DF22A14C5190E0001DEFA /* RKOrderedDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 250DF22814C5190E0001DEFA /* RKOrderedDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 250DF22B14C5190E0001DEFA /* RKOrderedDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 250DF22814C5190E0001DEFA /* RKOrderedDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 250DF22C14C5190E0001DEFA /* RKOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 250DF22914C5190E0001DEFA /* RKOrderedDictionary.m */; }; + 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 */; }; 25160D2814564E820060A5C5 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160D2714564E820060A5C5 /* SenTestingKit.framework */; }; 25160D2A14564E820060A5C5 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160D2914564E820060A5C5 /* UIKit.framework */; }; @@ -17,7 +49,6 @@ 25160DD5145650490060A5C5 /* CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D46145650490060A5C5 /* CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DD6145650490060A5C5 /* NSManagedObject+ActiveRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D47145650490060A5C5 /* NSManagedObject+ActiveRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DD7145650490060A5C5 /* NSManagedObject+ActiveRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D48145650490060A5C5 /* NSManagedObject+ActiveRecord.m */; }; - 25160DD8145650490060A5C5 /* RKManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D49145650490060A5C5 /* RKManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DD9145650490060A5C5 /* RKManagedObjectLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D4A145650490060A5C5 /* RKManagedObjectLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DDA145650490060A5C5 /* RKManagedObjectLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D4B145650490060A5C5 /* RKManagedObjectLoader.m */; }; 25160DDB145650490060A5C5 /* RKManagedObjectMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D4C145650490060A5C5 /* RKManagedObjectMapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -33,12 +64,10 @@ 25160DE5145650490060A5C5 /* RKObjectPropertyInspector+CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D56145650490060A5C5 /* RKObjectPropertyInspector+CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DE6145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D57145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m */; }; 25160DE7145650490060A5C5 /* Network.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D59145650490060A5C5 /* Network.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160DE8145650490060A5C5 /* NSData+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5A145650490060A5C5 /* NSData+MD5.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160DE9145650490060A5C5 /* NSData+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5B145650490060A5C5 /* NSData+MD5.m */; }; + 25160DE8145650490060A5C5 /* NSData+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5A145650490060A5C5 /* NSData+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160DE9145650490060A5C5 /* NSData+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5B145650490060A5C5 /* NSData+RKAdditions.m */; }; 25160DEA145650490060A5C5 /* NSDictionary+RKRequestSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5C145650490060A5C5 /* NSDictionary+RKRequestSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DEB145650490060A5C5 /* NSDictionary+RKRequestSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5D145650490060A5C5 /* NSDictionary+RKRequestSerialization.m */; }; - 25160DEC145650490060A5C5 /* NSString+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5E145650490060A5C5 /* NSString+MD5.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160DED145650490060A5C5 /* NSString+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5F145650490060A5C5 /* NSString+MD5.m */; }; 25160DEE145650490060A5C5 /* RKClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D60145650490060A5C5 /* RKClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160DEF145650490060A5C5 /* RKClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D61145650490060A5C5 /* RKClient.m */; }; 25160DF0145650490060A5C5 /* RKNotifications.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D62145650490060A5C5 /* RKNotifications.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -104,21 +133,17 @@ 25160E2C145650490060A5C5 /* RKParserRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D9F145650490060A5C5 /* RKParserRegistry.m */; }; 25160E2D145650490060A5C5 /* RKRouter.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA0145650490060A5C5 /* RKRouter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E2E145650490060A5C5 /* RestKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA1145650490060A5C5 /* RestKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160E2F145650490060A5C5 /* Errors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA3145650490060A5C5 /* Errors.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160E30145650490060A5C5 /* Errors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DA4145650490060A5C5 /* Errors.m */; }; 25160E31145650490060A5C5 /* lcl_config_components.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA5145650490060A5C5 /* lcl_config_components.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E32145650490060A5C5 /* lcl_config_extensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA6145650490060A5C5 /* lcl_config_extensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E33145650490060A5C5 /* lcl_config_logger.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA7145650490060A5C5 /* lcl_config_logger.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E34145650490060A5C5 /* NSDictionary+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA8145650490060A5C5 /* NSDictionary+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E35145650490060A5C5 /* NSDictionary+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DA9145650490060A5C5 /* NSDictionary+RKAdditions.m */; }; - 25160E36145650490060A5C5 /* NSString+RestKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAA145650490060A5C5 /* NSString+RestKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160E37145650490060A5C5 /* NSString+RestKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAB145650490060A5C5 /* NSString+RestKit.m */; }; - 25160E38145650490060A5C5 /* NSURL+RestKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAC145650490060A5C5 /* NSURL+RestKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160E39145650490060A5C5 /* NSURL+RestKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAD145650490060A5C5 /* NSURL+RestKit.m */; }; + 25160E36145650490060A5C5 /* NSString+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAA145650490060A5C5 /* NSString+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160E37145650490060A5C5 /* NSString+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAB145650490060A5C5 /* NSString+RKAdditions.m */; }; + 25160E38145650490060A5C5 /* NSURL+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAC145650490060A5C5 /* NSURL+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160E39145650490060A5C5 /* NSURL+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAD145650490060A5C5 /* NSURL+RKAdditions.m */; }; 25160E3A145650490060A5C5 /* RKJSONParserJSONKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DB0145650490060A5C5 /* RKJSONParserJSONKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E3B145650490060A5C5 /* RKJSONParserJSONKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DB1145650490060A5C5 /* RKJSONParserJSONKit.m */; }; - 25160E42145650490060A5C5 /* RKXMLParserLibXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DB9145650490060A5C5 /* RKXMLParserLibXML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160E43145650490060A5C5 /* RKXMLParserLibXML.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DBA145650490060A5C5 /* RKXMLParserLibXML.m */; }; 25160E44145650490060A5C5 /* RestKit-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 25160DBB145650490060A5C5 /* RestKit-Prefix.pch */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E45145650490060A5C5 /* RKAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DBC145650490060A5C5 /* RKAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160E46145650490060A5C5 /* RKAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DBD145650490060A5C5 /* RKAlert.m */; }; @@ -249,7 +274,6 @@ 25160F69145655D10060A5C5 /* CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D46145650490060A5C5 /* CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F6A145655D10060A5C5 /* NSManagedObject+ActiveRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D47145650490060A5C5 /* NSManagedObject+ActiveRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F6B145655D10060A5C5 /* NSManagedObject+ActiveRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D48145650490060A5C5 /* NSManagedObject+ActiveRecord.m */; }; - 25160F6C145655D10060A5C5 /* RKManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D49145650490060A5C5 /* RKManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F6D145655D10060A5C5 /* RKManagedObjectLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D4A145650490060A5C5 /* RKManagedObjectLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F6E145655D10060A5C5 /* RKManagedObjectLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D4B145650490060A5C5 /* RKManagedObjectLoader.m */; }; 25160F6F145655D10060A5C5 /* RKManagedObjectMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D4C145650490060A5C5 /* RKManagedObjectMapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -266,21 +290,17 @@ 25160F7A145655D10060A5C5 /* RKObjectPropertyInspector+CoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D57145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m */; }; 25160F7C145657220060A5C5 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160F7B145657220060A5C5 /* SystemConfiguration.framework */; }; 25160F7E145657300060A5C5 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160F7D1456572F0060A5C5 /* Cocoa.framework */; }; - 25160F7F145657650060A5C5 /* NSData+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5A145650490060A5C5 /* NSData+MD5.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F80145657650060A5C5 /* NSData+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5B145650490060A5C5 /* NSData+MD5.m */; }; - 25160F81145657650060A5C5 /* NSString+MD5.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5E145650490060A5C5 /* NSString+MD5.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F82145657650060A5C5 /* NSString+MD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5F145650490060A5C5 /* NSString+MD5.m */; }; - 25160F83145657650060A5C5 /* Errors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA3145650490060A5C5 /* Errors.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F84145657650060A5C5 /* Errors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DA4145650490060A5C5 /* Errors.m */; }; + 25160F7F145657650060A5C5 /* NSData+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160D5A145650490060A5C5 /* NSData+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160F80145657650060A5C5 /* NSData+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160D5B145650490060A5C5 /* NSData+RKAdditions.m */; }; 25160F85145657650060A5C5 /* lcl_config_components.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA5145650490060A5C5 /* lcl_config_components.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F86145657650060A5C5 /* lcl_config_extensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA6145650490060A5C5 /* lcl_config_extensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F87145657650060A5C5 /* lcl_config_logger.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA7145650490060A5C5 /* lcl_config_logger.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F88145657650060A5C5 /* NSDictionary+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DA8145650490060A5C5 /* NSDictionary+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F89145657650060A5C5 /* NSDictionary+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DA9145650490060A5C5 /* NSDictionary+RKAdditions.m */; }; - 25160F8A145657650060A5C5 /* NSString+RestKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAA145650490060A5C5 /* NSString+RestKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F8B145657650060A5C5 /* NSString+RestKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAB145650490060A5C5 /* NSString+RestKit.m */; }; - 25160F8C145657650060A5C5 /* NSURL+RestKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAC145650490060A5C5 /* NSURL+RestKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F8D145657650060A5C5 /* NSURL+RestKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAD145650490060A5C5 /* NSURL+RestKit.m */; }; + 25160F8A145657650060A5C5 /* NSString+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAA145650490060A5C5 /* NSString+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160F8B145657650060A5C5 /* NSString+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAB145650490060A5C5 /* NSString+RKAdditions.m */; }; + 25160F8C145657650060A5C5 /* NSURL+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DAC145650490060A5C5 /* NSURL+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25160F8D145657650060A5C5 /* NSURL+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DAD145650490060A5C5 /* NSURL+RKAdditions.m */; }; 25160F8E1456576C0060A5C5 /* RKAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DBC145650490060A5C5 /* RKAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F8F1456576C0060A5C5 /* RKAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DBD145650490060A5C5 /* RKAlert.m */; }; 25160F901456576C0060A5C5 /* RKDotNetDateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DBE145650490060A5C5 /* RKDotNetDateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -296,22 +316,19 @@ 25160F9A1456576C0060A5C5 /* RKSearchEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DC8145650490060A5C5 /* RKSearchEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F9B1456576C0060A5C5 /* RKSearchEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DC9145650490060A5C5 /* RKSearchEngine.m */; }; 25160F9C1456576C0060A5C5 /* Support.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DCA145650490060A5C5 /* Support.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 25160F9D145657720060A5C5 /* RKXMLParserLibXML.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DBA145650490060A5C5 /* RKXMLParserLibXML.m */; }; 25160F9E1456577F0060A5C5 /* RKJSONParserJSONKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160DB0145650490060A5C5 /* RKJSONParserJSONKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25160F9F1456577F0060A5C5 /* RKJSONParserJSONKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160DB1145650490060A5C5 /* RKJSONParserJSONKit.m */; }; 25160FA1145658BC0060A5C5 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160FA0145658BC0060A5C5 /* libxml2.dylib */; }; - 251610581456F2330060A5C5 /* RKManagedObjectLoaderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderSpec.m */; }; - 251610591456F2330060A5C5 /* RKManagedObjectLoaderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderSpec.m */; }; - 2516105A1456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m */; }; - 2516105B1456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m */; }; - 2516105C1456F2330060A5C5 /* RKManagedObjectMappingSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC91456F2330060A5C5 /* RKManagedObjectMappingSpec.m */; }; - 2516105D1456F2330060A5C5 /* RKManagedObjectMappingSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC91456F2330060A5C5 /* RKManagedObjectMappingSpec.m */; }; - 2516105E1456F2330060A5C5 /* RKManagedObjectSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCA1456F2330060A5C5 /* RKManagedObjectSpec.m */; }; - 2516105F1456F2330060A5C5 /* RKManagedObjectSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCA1456F2330060A5C5 /* RKManagedObjectSpec.m */; }; - 251610601456F2330060A5C5 /* RKManagedObjectStoreSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreSpec.m */; }; - 251610611456F2330060A5C5 /* RKManagedObjectStoreSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreSpec.m */; }; - 251610621456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m */; }; - 251610631456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m */; }; + 251610581456F2330060A5C5 /* RKManagedObjectLoaderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */; }; + 251610591456F2330060A5C5 /* RKManagedObjectLoaderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */; }; + 2516105A1456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */; }; + 2516105B1456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */; }; + 2516105C1456F2330060A5C5 /* RKManagedObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC91456F2330060A5C5 /* RKManagedObjectMappingTest.m */; }; + 2516105D1456F2330060A5C5 /* RKManagedObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FC91456F2330060A5C5 /* RKManagedObjectMappingTest.m */; }; + 251610601456F2330060A5C5 /* RKManagedObjectStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */; }; + 251610611456F2330060A5C5 /* RKManagedObjectStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */; }; + 251610621456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m */; }; + 251610631456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m */; }; 251610641456F2330060A5C5 /* blake.png in Resources */ = {isa = PBXBuildFile; fileRef = 25160FCF1456F2330060A5C5 /* blake.png */; }; 251610651456F2330060A5C5 /* blake.png in Resources */ = {isa = PBXBuildFile; fileRef = 25160FCF1456F2330060A5C5 /* blake.png */; }; 251610661456F2330060A5C5 /* ArrayOfNestedDictionaries.json in Resources */ = {isa = PBXBuildFile; fileRef = 25160FD11456F2330060A5C5 /* ArrayOfNestedDictionaries.json */; }; @@ -358,7 +375,6 @@ 2516108F1456F2330060A5C5 /* user.json in Resources */ = {isa = PBXBuildFile; fileRef = 25160FE71456F2330060A5C5 /* user.json */; }; 251610901456F2330060A5C5 /* users.json in Resources */ = {isa = PBXBuildFile; fileRef = 25160FE81456F2330060A5C5 /* users.json */; }; 251610911456F2330060A5C5 /* users.json in Resources */ = {isa = PBXBuildFile; fileRef = 25160FE81456F2330060A5C5 /* users.json */; }; - 251610921456F2330060A5C5 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 25160FEA1456F2330060A5C5 /* .gitignore */; }; 251610931456F2330060A5C5 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 25160FEA1456F2330060A5C5 /* .gitignore */; }; 251610961456F2330060A5C5 /* attributes_without_text_content.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25160FED1456F2330060A5C5 /* attributes_without_text_content.xml */; }; 251610971456F2330060A5C5 /* attributes_without_text_content.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25160FED1456F2330060A5C5 /* attributes_without_text_content.xml */; }; @@ -388,135 +404,344 @@ 251610AF1456F2330060A5C5 /* RKMappableAssociation.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610001456F2330060A5C5 /* RKMappableAssociation.m */; }; 251610B01456F2330060A5C5 /* RKMappableObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610021456F2330060A5C5 /* RKMappableObject.m */; }; 251610B11456F2330060A5C5 /* RKMappableObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610021456F2330060A5C5 /* RKMappableObject.m */; }; - 251610B21456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610041456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m */; }; - 251610B31456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610041456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m */; }; - 251610B41456F2330060A5C5 /* RKObjectMapperSpecModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610061456F2330060A5C5 /* RKObjectMapperSpecModel.m */; }; - 251610B51456F2330060A5C5 /* RKObjectMapperSpecModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610061456F2330060A5C5 /* RKObjectMapperSpecModel.m */; }; + 251610B21456F2330060A5C5 /* RKObjectLoaderTestResultModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610041456F2330060A5C5 /* RKObjectLoaderTestResultModel.m */; }; + 251610B31456F2330060A5C5 /* RKObjectLoaderTestResultModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610041456F2330060A5C5 /* RKObjectLoaderTestResultModel.m */; }; + 251610B41456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610061456F2330060A5C5 /* RKObjectMapperTestModel.m */; }; + 251610B51456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610061456F2330060A5C5 /* RKObjectMapperTestModel.m */; }; 251610B61456F2330060A5C5 /* RKParent.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610081456F2330060A5C5 /* RKParent.m */; }; 251610B71456F2330060A5C5 /* RKParent.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610081456F2330060A5C5 /* RKParent.m */; }; 251610B81456F2330060A5C5 /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516100A1456F2330060A5C5 /* RKResident.m */; }; 251610B91456F2330060A5C5 /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516100A1456F2330060A5C5 /* RKResident.m */; }; - 251610BE1456F2330060A5C5 /* RKAuthenticationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610101456F2330060A5C5 /* RKAuthenticationSpec.m */; }; - 251610BF1456F2330060A5C5 /* RKAuthenticationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610101456F2330060A5C5 /* RKAuthenticationSpec.m */; }; - 251610C01456F2330060A5C5 /* RKClientSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610111456F2330060A5C5 /* RKClientSpec.m */; }; - 251610C11456F2330060A5C5 /* RKClientSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610111456F2330060A5C5 /* RKClientSpec.m */; }; - 251610C21456F2330060A5C5 /* RKOAuthClientSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610121456F2330060A5C5 /* RKOAuthClientSpec.m */; }; - 251610C31456F2330060A5C5 /* RKOAuthClientSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610121456F2330060A5C5 /* RKOAuthClientSpec.m */; }; - 251610C41456F2330060A5C5 /* RKParamsAttachmentSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610131456F2330060A5C5 /* RKParamsAttachmentSpec.m */; }; - 251610C51456F2330060A5C5 /* RKParamsAttachmentSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610131456F2330060A5C5 /* RKParamsAttachmentSpec.m */; }; - 251610C61456F2330060A5C5 /* RKParamsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610141456F2330060A5C5 /* RKParamsSpec.m */; }; - 251610C71456F2330060A5C5 /* RKParamsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610141456F2330060A5C5 /* RKParamsSpec.m */; }; - 251610CA1456F2330060A5C5 /* RKRequestQueueSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610171456F2330060A5C5 /* RKRequestQueueSpec.m */; }; - 251610CB1456F2330060A5C5 /* RKRequestQueueSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610171456F2330060A5C5 /* RKRequestQueueSpec.m */; }; - 251610CC1456F2330060A5C5 /* RKRequestSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610181456F2330060A5C5 /* RKRequestSpec.m */; }; - 251610CD1456F2330060A5C5 /* RKRequestSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610181456F2330060A5C5 /* RKRequestSpec.m */; }; - 251610CE1456F2330060A5C5 /* RKResponseSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610191456F2330060A5C5 /* RKResponseSpec.m */; }; - 251610CF1456F2330060A5C5 /* RKResponseSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610191456F2330060A5C5 /* RKResponseSpec.m */; }; - 251610D01456F2330060A5C5 /* RKURLSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101A1456F2330060A5C5 /* RKURLSpec.m */; }; - 251610D11456F2330060A5C5 /* RKURLSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101A1456F2330060A5C5 /* RKURLSpec.m */; }; - 251610D21456F2330060A5C5 /* RKDynamicObjectMappingSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingSpec.m */; }; - 251610D31456F2330060A5C5 /* RKDynamicObjectMappingSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingSpec.m */; }; - 251610D41456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2516101D1456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm */; }; - 251610D51456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2516101D1456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm */; }; - 251610D61456F2330060A5C5 /* RKObjectLoaderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101E1456F2330060A5C5 /* RKObjectLoaderSpec.m */; }; - 251610D71456F2330060A5C5 /* RKObjectLoaderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101E1456F2330060A5C5 /* RKObjectLoaderSpec.m */; }; - 251610D81456F2330060A5C5 /* RKObjectManagerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101F1456F2330060A5C5 /* RKObjectManagerSpec.m */; }; - 251610D91456F2330060A5C5 /* RKObjectManagerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101F1456F2330060A5C5 /* RKObjectManagerSpec.m */; }; - 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenSpec.m */; }; - 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenSpec.m */; }; - 251610DE1456F2330060A5C5 /* RKObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610221456F2330060A5C5 /* RKObjectMappingOperationSpec.m */; }; - 251610DF1456F2330060A5C5 /* RKObjectMappingOperationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610221456F2330060A5C5 /* RKObjectMappingOperationSpec.m */; }; - 251610E01456F2330060A5C5 /* RKObjectMappingProviderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610231456F2330060A5C5 /* RKObjectMappingProviderSpec.m */; }; - 251610E11456F2330060A5C5 /* RKObjectMappingProviderSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610231456F2330060A5C5 /* RKObjectMappingProviderSpec.m */; }; - 251610E21456F2330060A5C5 /* RKObjectMappingResultSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610241456F2330060A5C5 /* RKObjectMappingResultSpec.m */; }; - 251610E31456F2330060A5C5 /* RKObjectMappingResultSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610241456F2330060A5C5 /* RKObjectMappingResultSpec.m */; }; - 251610E41456F2330060A5C5 /* RKObjectRouterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610251456F2330060A5C5 /* RKObjectRouterSpec.m */; }; - 251610E51456F2330060A5C5 /* RKObjectRouterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610251456F2330060A5C5 /* RKObjectRouterSpec.m */; }; - 251610E61456F2330060A5C5 /* RKObjectSerializerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610261456F2330060A5C5 /* RKObjectSerializerSpec.m */; }; - 251610E71456F2330060A5C5 /* RKObjectSerializerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610261456F2330060A5C5 /* RKObjectSerializerSpec.m */; }; - 251610E81456F2330060A5C5 /* RKParserRegistrySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610271456F2330060A5C5 /* RKParserRegistrySpec.m */; }; - 251610E91456F2330060A5C5 /* RKParserRegistrySpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610271456F2330060A5C5 /* RKParserRegistrySpec.m */; }; - 251610F01456F2340060A5C5 /* RKSpecEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610361456F2330060A5C5 /* RKSpecEnvironment.m */; }; - 251610F11456F2340060A5C5 /* RKSpecEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610361456F2330060A5C5 /* RKSpecEnvironment.m */; }; - 251610F21456F2340060A5C5 /* RKSpecResponseLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610381456F2330060A5C5 /* RKSpecResponseLoader.m */; }; - 251610F31456F2340060A5C5 /* RKSpecResponseLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610381456F2330060A5C5 /* RKSpecResponseLoader.m */; }; - 251610F51456F2340060A5C5 /* set_ip_address.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 251610391456F2330060A5C5 /* set_ip_address.scpt */; }; + 251610BE1456F2330060A5C5 /* RKAuthenticationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610101456F2330060A5C5 /* RKAuthenticationTest.m */; }; + 251610BF1456F2330060A5C5 /* RKAuthenticationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610101456F2330060A5C5 /* RKAuthenticationTest.m */; }; + 251610C01456F2330060A5C5 /* RKClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610111456F2330060A5C5 /* RKClientTest.m */; }; + 251610C11456F2330060A5C5 /* RKClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610111456F2330060A5C5 /* RKClientTest.m */; }; + 251610C21456F2330060A5C5 /* RKOAuthClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610121456F2330060A5C5 /* RKOAuthClientTest.m */; }; + 251610C31456F2330060A5C5 /* RKOAuthClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610121456F2330060A5C5 /* RKOAuthClientTest.m */; }; + 251610C41456F2330060A5C5 /* RKParamsAttachmentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610131456F2330060A5C5 /* RKParamsAttachmentTest.m */; }; + 251610C51456F2330060A5C5 /* RKParamsAttachmentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610131456F2330060A5C5 /* RKParamsAttachmentTest.m */; }; + 251610C61456F2330060A5C5 /* RKParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610141456F2330060A5C5 /* RKParamsTest.m */; }; + 251610C71456F2330060A5C5 /* RKParamsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610141456F2330060A5C5 /* RKParamsTest.m */; }; + 251610CA1456F2330060A5C5 /* RKRequestQueueTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610171456F2330060A5C5 /* RKRequestQueueTest.m */; }; + 251610CB1456F2330060A5C5 /* RKRequestQueueTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610171456F2330060A5C5 /* RKRequestQueueTest.m */; }; + 251610CC1456F2330060A5C5 /* RKRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610181456F2330060A5C5 /* RKRequestTest.m */; }; + 251610CD1456F2330060A5C5 /* RKRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610181456F2330060A5C5 /* RKRequestTest.m */; }; + 251610CE1456F2330060A5C5 /* RKResponseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610191456F2330060A5C5 /* RKResponseTest.m */; }; + 251610CF1456F2330060A5C5 /* RKResponseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610191456F2330060A5C5 /* RKResponseTest.m */; }; + 251610D01456F2330060A5C5 /* RKURLTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101A1456F2330060A5C5 /* RKURLTest.m */; }; + 251610D11456F2330060A5C5 /* RKURLTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101A1456F2330060A5C5 /* RKURLTest.m */; }; + 251610D21456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */; }; + 251610D31456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */; }; + 251610D61456F2330060A5C5 /* RKObjectLoaderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101E1456F2330060A5C5 /* RKObjectLoaderTest.m */; }; + 251610D71456F2330060A5C5 /* RKObjectLoaderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101E1456F2330060A5C5 /* RKObjectLoaderTest.m */; }; + 251610D81456F2330060A5C5 /* RKObjectManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */; }; + 251610D91456F2330060A5C5 /* RKObjectManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */; }; + 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */; }; + 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */; }; + 251610DE1456F2330060A5C5 /* RKObjectMappingOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610221456F2330060A5C5 /* RKObjectMappingOperationTest.m */; }; + 251610DF1456F2330060A5C5 /* RKObjectMappingOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610221456F2330060A5C5 /* RKObjectMappingOperationTest.m */; }; + 251610E01456F2330060A5C5 /* RKObjectMappingProviderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610231456F2330060A5C5 /* RKObjectMappingProviderTest.m */; }; + 251610E11456F2330060A5C5 /* RKObjectMappingProviderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610231456F2330060A5C5 /* RKObjectMappingProviderTest.m */; }; + 251610E21456F2330060A5C5 /* RKObjectMappingResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610241456F2330060A5C5 /* RKObjectMappingResultTest.m */; }; + 251610E31456F2330060A5C5 /* RKObjectMappingResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610241456F2330060A5C5 /* RKObjectMappingResultTest.m */; }; + 251610E41456F2330060A5C5 /* RKObjectRouterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610251456F2330060A5C5 /* RKObjectRouterTest.m */; }; + 251610E51456F2330060A5C5 /* RKObjectRouterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610251456F2330060A5C5 /* RKObjectRouterTest.m */; }; + 251610E61456F2330060A5C5 /* RKObjectSerializerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610261456F2330060A5C5 /* RKObjectSerializerTest.m */; }; + 251610E71456F2330060A5C5 /* RKObjectSerializerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610261456F2330060A5C5 /* RKObjectSerializerTest.m */; }; + 251610E81456F2330060A5C5 /* RKParserRegistryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610271456F2330060A5C5 /* RKParserRegistryTest.m */; }; + 251610E91456F2330060A5C5 /* RKParserRegistryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610271456F2330060A5C5 /* RKParserRegistryTest.m */; }; + 251610F01456F2340060A5C5 /* RKTestEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610361456F2330060A5C5 /* RKTestEnvironment.m */; }; + 251610F11456F2340060A5C5 /* RKTestEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610361456F2330060A5C5 /* RKTestEnvironment.m */; }; 251611031456F2340060A5C5 /* authentication.rb in Resources */ = {isa = PBXBuildFile; fileRef = 2516104B1456F2330060A5C5 /* authentication.rb */; }; 251611051456F2340060A5C5 /* etags.rb in Resources */ = {isa = PBXBuildFile; fileRef = 2516104C1456F2330060A5C5 /* etags.rb */; }; 251611071456F2340060A5C5 /* oauth2.rb in Resources */ = {isa = PBXBuildFile; fileRef = 2516104D1456F2330060A5C5 /* oauth2.rb */; }; 251611091456F2340060A5C5 /* timeout.rb in Resources */ = {isa = PBXBuildFile; fileRef = 2516104E1456F2330060A5C5 /* timeout.rb */; }; 2516110B1456F2340060A5C5 /* restkit.rb in Resources */ = {isa = PBXBuildFile; fileRef = 2516104F1456F2330060A5C5 /* restkit.rb */; }; 2516110D1456F2340060A5C5 /* server.rb in Resources */ = {isa = PBXBuildFile; fileRef = 251610501456F2330060A5C5 /* server.rb */; }; - 2516110E1456F2340060A5C5 /* NSDictionary+RKRequestSerializationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationSpec.m */; }; - 2516110F1456F2340060A5C5 /* NSDictionary+RKRequestSerializationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationSpec.m */; }; - 251611101456F2340060A5C5 /* NSStringRestKitSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610531456F2330060A5C5 /* NSStringRestKitSpec.m */; }; - 251611111456F2340060A5C5 /* NSStringRestKitSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610531456F2330060A5C5 /* NSStringRestKitSpec.m */; }; - 251611121456F2340060A5C5 /* RKDotNetDateFormatterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610541456F2330060A5C5 /* RKDotNetDateFormatterSpec.m */; }; - 251611131456F2340060A5C5 /* RKDotNetDateFormatterSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610541456F2330060A5C5 /* RKDotNetDateFormatterSpec.m */; }; - 251611141456F2340060A5C5 /* RKJSONParserJSONKitSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610551456F2330060A5C5 /* RKJSONParserJSONKitSpec.m */; }; - 251611151456F2340060A5C5 /* RKJSONParserJSONKitSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610551456F2330060A5C5 /* RKJSONParserJSONKitSpec.m */; }; - 251611161456F2340060A5C5 /* RKPathMatcherSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610561456F2330060A5C5 /* RKPathMatcherSpec.m */; }; - 251611171456F2340060A5C5 /* RKPathMatcherSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610561456F2330060A5C5 /* RKPathMatcherSpec.m */; }; - 251611181456F2340060A5C5 /* RKXMLParserSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610571456F2330060A5C5 /* RKXMLParserSpec.m */; }; - 251611191456F2340060A5C5 /* RKXMLParserSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610571456F2330060A5C5 /* RKXMLParserSpec.m */; }; - 251611271456F4A90060A5C5 /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160D1614564E810060A5C5 /* libRestKit.a */; }; + 2516110E1456F2340060A5C5 /* NSDictionary+RKRequestSerializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationTest.m */; }; + 2516110F1456F2340060A5C5 /* NSDictionary+RKRequestSerializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationTest.m */; }; + 251611101456F2340060A5C5 /* NSStringRestKitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610531456F2330060A5C5 /* NSStringRestKitTest.m */; }; + 251611111456F2340060A5C5 /* NSStringRestKitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610531456F2330060A5C5 /* NSStringRestKitTest.m */; }; + 251611121456F2340060A5C5 /* RKDotNetDateFormatterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610541456F2330060A5C5 /* RKDotNetDateFormatterTest.m */; }; + 251611131456F2340060A5C5 /* RKDotNetDateFormatterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610541456F2330060A5C5 /* RKDotNetDateFormatterTest.m */; }; + 251611141456F2340060A5C5 /* RKJSONParserJSONKitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610551456F2330060A5C5 /* RKJSONParserJSONKitTest.m */; }; + 251611151456F2340060A5C5 /* RKJSONParserJSONKitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610551456F2330060A5C5 /* RKJSONParserJSONKitTest.m */; }; + 251611161456F2340060A5C5 /* RKPathMatcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610561456F2330060A5C5 /* RKPathMatcherTest.m */; }; + 251611171456F2340060A5C5 /* RKPathMatcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610561456F2330060A5C5 /* RKPathMatcherTest.m */; }; + 251611181456F2340060A5C5 /* RKXMLParserTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610571456F2330060A5C5 /* RKXMLParserTest.m */; }; + 251611191456F2340060A5C5 /* RKXMLParserTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610571456F2330060A5C5 /* RKXMLParserTest.m */; }; 251611291456F50F0060A5C5 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 251611281456F50F0060A5C5 /* SystemConfiguration.framework */; }; 2516112B1456F5170060A5C5 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2516112A1456F5170060A5C5 /* CFNetwork.framework */; }; 2516112C1456F51D0060A5C5 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 25160F161456538B0060A5C5 /* libxml2.dylib */; }; 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 */; }; - 25A341C0147C2F370009758D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25A34191147C2F370009758D /* InfoPlist.strings */; }; - 25A341C1147C2F370009758D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25A34191147C2F370009758D /* InfoPlist.strings */; }; - 25A341C2147C2F370009758D /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34194147C2F370009758D /* NSInvocation+OCMAdditions.m */; }; - 25A341C3147C2F370009758D /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34194147C2F370009758D /* NSInvocation+OCMAdditions.m */; }; - 25A341C4147C2F370009758D /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34196147C2F370009758D /* NSMethodSignature+OCMAdditions.m */; }; - 25A341C5147C2F370009758D /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34196147C2F370009758D /* NSMethodSignature+OCMAdditions.m */; }; - 25A341C6147C2F370009758D /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34198147C2F370009758D /* NSNotificationCenter+OCMAdditions.m */; }; - 25A341C7147C2F370009758D /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A34198147C2F370009758D /* NSNotificationCenter+OCMAdditions.m */; }; - 25A341C8147C2F370009758D /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419A147C2F370009758D /* OCClassMockObject.m */; }; - 25A341C9147C2F370009758D /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419A147C2F370009758D /* OCClassMockObject.m */; }; - 25A341CA147C2F370009758D /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419C147C2F370009758D /* OCMArg.m */; }; - 25A341CB147C2F370009758D /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419C147C2F370009758D /* OCMArg.m */; }; - 25A341CC147C2F370009758D /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419E147C2F370009758D /* OCMBlockCaller.m */; }; - 25A341CD147C2F370009758D /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A3419E147C2F370009758D /* OCMBlockCaller.m */; }; - 25A341CE147C2F370009758D /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A0147C2F370009758D /* OCMBoxedReturnValueProvider.m */; }; - 25A341CF147C2F370009758D /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A0147C2F370009758D /* OCMBoxedReturnValueProvider.m */; }; - 25A341D0147C2F370009758D /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A2147C2F370009758D /* OCMConstraint.m */; }; - 25A341D1147C2F370009758D /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A2147C2F370009758D /* OCMConstraint.m */; }; - 25A341D2147C2F370009758D /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A4147C2F370009758D /* OCMExceptionReturnValueProvider.m */; }; - 25A341D3147C2F370009758D /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A4147C2F370009758D /* OCMExceptionReturnValueProvider.m */; }; - 25A341D4147C2F370009758D /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A6147C2F370009758D /* OCMIndirectReturnValueProvider.m */; }; - 25A341D5147C2F370009758D /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A6147C2F370009758D /* OCMIndirectReturnValueProvider.m */; }; - 25A341D6147C2F370009758D /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A8147C2F370009758D /* OCMNotificationPoster.m */; }; - 25A341D7147C2F370009758D /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341A8147C2F370009758D /* OCMNotificationPoster.m */; }; - 25A341D8147C2F370009758D /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341AA147C2F370009758D /* OCMObserverRecorder.m */; }; - 25A341D9147C2F370009758D /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341AA147C2F370009758D /* OCMObserverRecorder.m */; }; - 25A341DA147C2F370009758D /* OCMock-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25A341AB147C2F370009758D /* OCMock-Info.plist */; }; - 25A341DB147C2F370009758D /* OCMock-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25A341AB147C2F370009758D /* OCMock-Info.plist */; }; - 25A341DC147C2F370009758D /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341AF147C2F370009758D /* OCMockObject.m */; }; - 25A341DD147C2F370009758D /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341AF147C2F370009758D /* OCMockObject.m */; }; - 25A341DE147C2F370009758D /* OCMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B1147C2F370009758D /* OCMockRecorder.m */; }; - 25A341DF147C2F370009758D /* OCMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B1147C2F370009758D /* OCMockRecorder.m */; }; - 25A341E0147C2F370009758D /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B3147C2F370009758D /* OCMPassByRefSetter.m */; }; - 25A341E1147C2F370009758D /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B3147C2F370009758D /* OCMPassByRefSetter.m */; }; - 25A341E2147C2F370009758D /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B5147C2F370009758D /* OCMRealObjectForwarder.m */; }; - 25A341E3147C2F370009758D /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B5147C2F370009758D /* OCMRealObjectForwarder.m */; }; - 25A341E4147C2F370009758D /* OCMReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B7147C2F370009758D /* OCMReturnValueProvider.m */; }; - 25A341E5147C2F370009758D /* OCMReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B7147C2F370009758D /* OCMReturnValueProvider.m */; }; - 25A341E6147C2F370009758D /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B9147C2F370009758D /* OCObserverMockObject.m */; }; - 25A341E7147C2F370009758D /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341B9147C2F370009758D /* OCObserverMockObject.m */; }; - 25A341E8147C2F370009758D /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BB147C2F370009758D /* OCPartialMockObject.m */; }; - 25A341E9147C2F370009758D /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BB147C2F370009758D /* OCPartialMockObject.m */; }; - 25A341EA147C2F370009758D /* OCPartialMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BD147C2F370009758D /* OCPartialMockRecorder.m */; }; - 25A341EB147C2F370009758D /* OCPartialMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BD147C2F370009758D /* OCPartialMockRecorder.m */; }; - 25A341EC147C2F370009758D /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BF147C2F370009758D /* OCProtocolMockObject.m */; }; - 25A341ED147C2F370009758D /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A341BF147C2F370009758D /* OCProtocolMockObject.m */; }; + 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 */; }; + 252EFB0914D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0614D98F4D004863C8 /* RKSearchableManagedObjectTest.m */; }; + 252EFB0A14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0714D98F4D004863C8 /* RKSearchWordObserverTest.m */; }; + 252EFB0B14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0714D98F4D004863C8 /* RKSearchWordObserverTest.m */; }; + 252EFB0D14D98F76004863C8 /* RKMutableBlockDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0C14D98F76004863C8 /* RKMutableBlockDictionaryTest.m */; }; + 252EFB0E14D98F76004863C8 /* RKMutableBlockDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB0C14D98F76004863C8 /* RKMutableBlockDictionaryTest.m */; }; + 252EFB1B14D9A7CB004863C8 /* NSBundle+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB1914D9A7CB004863C8 /* NSBundle+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252EFB1C14D9A7CB004863C8 /* NSBundle+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB1914D9A7CB004863C8 /* NSBundle+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252EFB1D14D9A7CB004863C8 /* NSBundle+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB1A14D9A7CB004863C8 /* NSBundle+RKAdditions.m */; }; + 252EFB1E14D9A7CB004863C8 /* NSBundle+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB1A14D9A7CB004863C8 /* NSBundle+RKAdditions.m */; }; + 252EFB2514D9B6F2004863C8 /* Testing.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB2414D9B6F2004863C8 /* Testing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252EFB2614D9B6F2004863C8 /* Testing.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB2414D9B6F2004863C8 /* Testing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252EFB2814DA0689004863C8 /* NakedEvents.json in Resources */ = {isa = PBXBuildFile; fileRef = 252EFB2714DA0689004863C8 /* NakedEvents.json */; }; + 252EFB2914DA0689004863C8 /* NakedEvents.json in Resources */ = {isa = PBXBuildFile; fileRef = 252EFB2714DA0689004863C8 /* NakedEvents.json */; }; + 253B495214E35D1A00B0483F /* RKTestFixture.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFB2014D9B35D004863C8 /* RKTestFixture.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 253B495F14E35EC300B0483F /* RKTestFixture.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFB2114D9B35D004863C8 /* RKTestFixture.m */; }; + 254A62B914AD544200939BEE /* RKObjectPaginator.h in Headers */ = {isa = PBXBuildFile; fileRef = 254A62B714AD544200939BEE /* RKObjectPaginator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 254A62BA14AD544200939BEE /* RKObjectPaginator.h in Headers */ = {isa = PBXBuildFile; fileRef = 254A62B714AD544200939BEE /* RKObjectPaginator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 254A62BB14AD544200939BEE /* RKObjectPaginator.m in Sources */ = {isa = PBXBuildFile; fileRef = 254A62B814AD544200939BEE /* RKObjectPaginator.m */; }; + 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 */; }; + 257ABAB315112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAAF15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.m */; }; + 257ABAB61511371E00CCAA76 /* NSManagedObject+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 257ABAB71511371E00CCAA76 /* NSManagedObject+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 257ABAB81511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */; }; + 257ABAB91511371E00CCAA76 /* NSManagedObject+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */; }; + 259C301715128079003066A2 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25EC1B0014F8078100C3CF3F /* CoreFoundation.framework */; }; + 259C3022151280A1003066A2 /* blackArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1ADE14F8022600C3CF3F /* blackArrow.png */; }; + 259C3023151280A1003066A2 /* blackArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1ADF14F8022600C3CF3F /* blackArrow@2x.png */; }; + 259C3024151280A1003066A2 /* blueArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE014F8022600C3CF3F /* blueArrow.png */; }; + 259C3025151280A1003066A2 /* blueArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE114F8022600C3CF3F /* blueArrow@2x.png */; }; + 259C3026151280A1003066A2 /* grayArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE214F8022600C3CF3F /* grayArrow.png */; }; + 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, ); }; }; 25B408281491CDDC00F21111 /* RKDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B408251491CDDB00F21111 /* RKDirectory.m */; }; 25B408291491CDDC00F21111 /* RKDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B408251491CDDB00F21111 /* RKDirectory.m */; }; + 25B6E91E14CF778D00B1E881 /* RKAbstractTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90314CF778D00B1E881 /* RKAbstractTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E92014CF778D00B1E881 /* RKAbstractTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E90414CF778D00B1E881 /* RKAbstractTableController.m */; }; + 25B6E92314CF778D00B1E881 /* RKAbstractTableController_Internals.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90614CF778D00B1E881 /* RKAbstractTableController_Internals.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E92514CF778D00B1E881 /* RKControlTableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90714CF778D00B1E881 /* RKControlTableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E92714CF778D00B1E881 /* RKControlTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E90814CF778D00B1E881 /* RKControlTableItem.m */; }; + 25B6E92914CF778D00B1E881 /* RKControlTableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90914CF778D00B1E881 /* RKControlTableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E92B14CF778D00B1E881 /* RKControlTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E90A14CF778D00B1E881 /* RKControlTableViewCell.m */; }; + 25B6E92D14CF778D00B1E881 /* RKFetchedResultsTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90B14CF778D00B1E881 /* RKFetchedResultsTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E92F14CF778D00B1E881 /* RKFetchedResultsTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E90C14CF778D00B1E881 /* RKFetchedResultsTableController.m */; }; + 25B6E93114CF778D00B1E881 /* RKForm.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90D14CF778D00B1E881 /* RKForm.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E93314CF778D00B1E881 /* RKForm.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E90E14CF778D00B1E881 /* RKForm.m */; }; + 25B6E93514CF778D00B1E881 /* RKFormSection.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E90F14CF778D00B1E881 /* RKFormSection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E93714CF778D00B1E881 /* RKFormSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91014CF778D00B1E881 /* RKFormSection.m */; }; + 25B6E93914CF778D00B1E881 /* RKTableController.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91114CF778D00B1E881 /* RKTableController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E93B14CF778D00B1E881 /* RKTableController.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91214CF778D00B1E881 /* RKTableController.m */; }; + 25B6E93D14CF778D00B1E881 /* RKTableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91314CF778D00B1E881 /* RKTableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E93F14CF778D00B1E881 /* RKTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91414CF778D00B1E881 /* RKTableItem.m */; }; + 25B6E94114CF778D00B1E881 /* RKTableSection.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91514CF778D00B1E881 /* RKTableSection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E94314CF778D00B1E881 /* RKTableSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91614CF778D00B1E881 /* RKTableSection.m */; }; + 25B6E94514CF778D00B1E881 /* RKTableViewCellMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91714CF778D00B1E881 /* RKTableViewCellMapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E94714CF778D00B1E881 /* RKTableViewCellMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91814CF778D00B1E881 /* RKTableViewCellMapping.m */; }; + 25B6E94914CF778D00B1E881 /* RKTableViewCellMappings.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91914CF778D00B1E881 /* RKTableViewCellMappings.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E94B14CF778D00B1E881 /* RKTableViewCellMappings.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91A14CF778D00B1E881 /* RKTableViewCellMappings.m */; }; + 25B6E94D14CF778D00B1E881 /* UI.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91B14CF778D00B1E881 /* UI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E94F14CF778D00B1E881 /* UIView+FindFirstResponder.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E91C14CF778D00B1E881 /* UIView+FindFirstResponder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95114CF778D00B1E881 /* UIView+FindFirstResponder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E91D14CF778D00B1E881 /* UIView+FindFirstResponder.m */; }; + 25B6E95514CF795D00B1E881 /* RKErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95414CF795D00B1E881 /* RKErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95614CF795D00B1E881 /* RKErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95414CF795D00B1E881 /* RKErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95814CF7A1C00B1E881 /* RKErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95714CF7A1C00B1E881 /* RKErrors.m */; }; + 25B6E95914CF7A1C00B1E881 /* RKErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95714CF7A1C00B1E881 /* RKErrors.m */; }; + 25B6E95C14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95D14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E95A14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E95E14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95B14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m */; }; + 25B6E95F14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E95B14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m */; }; + 25B6E9A414CF829400B1E881 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25B6E97B14CF829400B1E881 /* InfoPlist.strings */; }; + 25B6E9A514CF829400B1E881 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25B6E97B14CF829400B1E881 /* InfoPlist.strings */; }; + 25B6E9A614CF829400B1E881 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E97E14CF829400B1E881 /* NSInvocation+OCMAdditions.m */; }; + 25B6E9A714CF829400B1E881 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E97E14CF829400B1E881 /* NSInvocation+OCMAdditions.m */; }; + 25B6E9A814CF829400B1E881 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98014CF829400B1E881 /* NSMethodSignature+OCMAdditions.m */; }; + 25B6E9A914CF829400B1E881 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98014CF829400B1E881 /* NSMethodSignature+OCMAdditions.m */; }; + 25B6E9AA14CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98114CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m */; }; + 25B6E9AB14CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98114CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m */; }; + 25B6E9AC14CF829400B1E881 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98314CF829400B1E881 /* OCClassMockObject.m */; }; + 25B6E9AD14CF829400B1E881 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98314CF829400B1E881 /* OCClassMockObject.m */; }; + 25B6E9AE14CF829400B1E881 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98414CF829400B1E881 /* OCMArg.m */; }; + 25B6E9AF14CF829400B1E881 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98414CF829400B1E881 /* OCMArg.m */; }; + 25B6E9B014CF829400B1E881 /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98614CF829400B1E881 /* OCMBlockCaller.m */; }; + 25B6E9B114CF829400B1E881 /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98614CF829400B1E881 /* OCMBlockCaller.m */; }; + 25B6E9B214CF829400B1E881 /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98814CF829400B1E881 /* OCMBoxedReturnValueProvider.m */; }; + 25B6E9B314CF829400B1E881 /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98814CF829400B1E881 /* OCMBoxedReturnValueProvider.m */; }; + 25B6E9B414CF829400B1E881 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98914CF829400B1E881 /* OCMConstraint.m */; }; + 25B6E9B514CF829400B1E881 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98914CF829400B1E881 /* OCMConstraint.m */; }; + 25B6E9B614CF829400B1E881 /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98B14CF829400B1E881 /* OCMExceptionReturnValueProvider.m */; }; + 25B6E9B714CF829400B1E881 /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98B14CF829400B1E881 /* OCMExceptionReturnValueProvider.m */; }; + 25B6E9B814CF829400B1E881 /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98D14CF829400B1E881 /* OCMIndirectReturnValueProvider.m */; }; + 25B6E9B914CF829400B1E881 /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98D14CF829400B1E881 /* OCMIndirectReturnValueProvider.m */; }; + 25B6E9BA14CF829400B1E881 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98F14CF829400B1E881 /* OCMNotificationPoster.m */; }; + 25B6E9BB14CF829400B1E881 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E98F14CF829400B1E881 /* OCMNotificationPoster.m */; }; + 25B6E9BC14CF829400B1E881 /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99114CF829400B1E881 /* OCMObserverRecorder.m */; }; + 25B6E9BD14CF829400B1E881 /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99114CF829400B1E881 /* OCMObserverRecorder.m */; }; + 25B6E9BE14CF829400B1E881 /* OCMock-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25B6E99214CF829400B1E881 /* OCMock-Info.plist */; }; + 25B6E9BF14CF829400B1E881 /* OCMock-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25B6E99214CF829400B1E881 /* OCMock-Info.plist */; }; + 25B6E9C014CF829400B1E881 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99414CF829400B1E881 /* OCMockObject.m */; }; + 25B6E9C114CF829400B1E881 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99414CF829400B1E881 /* OCMockObject.m */; }; + 25B6E9C214CF829400B1E881 /* OCMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99514CF829400B1E881 /* OCMockRecorder.m */; }; + 25B6E9C314CF829400B1E881 /* OCMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99514CF829400B1E881 /* OCMockRecorder.m */; }; + 25B6E9C414CF829400B1E881 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99714CF829400B1E881 /* OCMPassByRefSetter.m */; }; + 25B6E9C514CF829400B1E881 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99714CF829400B1E881 /* OCMPassByRefSetter.m */; }; + 25B6E9C614CF829400B1E881 /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99914CF829400B1E881 /* OCMRealObjectForwarder.m */; }; + 25B6E9C714CF829400B1E881 /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99914CF829400B1E881 /* OCMRealObjectForwarder.m */; }; + 25B6E9C814CF829400B1E881 /* OCMReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99B14CF829400B1E881 /* OCMReturnValueProvider.m */; }; + 25B6E9C914CF829400B1E881 /* OCMReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99B14CF829400B1E881 /* OCMReturnValueProvider.m */; }; + 25B6E9CA14CF829400B1E881 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99D14CF829400B1E881 /* OCObserverMockObject.m */; }; + 25B6E9CB14CF829400B1E881 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99D14CF829400B1E881 /* OCObserverMockObject.m */; }; + 25B6E9CC14CF829400B1E881 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99F14CF829400B1E881 /* OCPartialMockObject.m */; }; + 25B6E9CD14CF829400B1E881 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E99F14CF829400B1E881 /* OCPartialMockObject.m */; }; + 25B6E9CE14CF829400B1E881 /* OCPartialMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9A114CF829400B1E881 /* OCPartialMockRecorder.m */; }; + 25B6E9CF14CF829400B1E881 /* OCPartialMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9A114CF829400B1E881 /* OCPartialMockRecorder.m */; }; + 25B6E9D014CF829400B1E881 /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9A314CF829400B1E881 /* OCProtocolMockObject.m */; }; + 25B6E9D114CF829400B1E881 /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9A314CF829400B1E881 /* OCProtocolMockObject.m */; }; + 25B6E9DB14CF912500B1E881 /* RKSearchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D614CF912500B1E881 /* RKSearchable.m */; }; + 25B6E9DC14CF912500B1E881 /* RKSearchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D614CF912500B1E881 /* RKSearchable.m */; }; + 25B6E9DD14CF912500B1E881 /* RKTestAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D814CF912500B1E881 /* RKTestAddress.m */; }; + 25B6E9DE14CF912500B1E881 /* RKTestAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9D814CF912500B1E881 /* RKTestAddress.m */; }; + 25B6E9DF14CF912500B1E881 /* RKTestUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9DA14CF912500B1E881 /* RKTestUser.m */; }; + 25B6E9E014CF912500B1E881 /* RKTestUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9DA14CF912500B1E881 /* RKTestUser.m */; }; + 25B6E9E914CF940700B1E881 /* RKManagedObjectSearchEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E114CF940600B1E881 /* RKManagedObjectSearchEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9EA14CF940700B1E881 /* RKManagedObjectSearchEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E114CF940600B1E881 /* RKManagedObjectSearchEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9EB14CF940700B1E881 /* RKManagedObjectSearchEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E214CF940600B1E881 /* RKManagedObjectSearchEngine.m */; }; + 25B6E9EC14CF940700B1E881 /* RKManagedObjectSearchEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E214CF940600B1E881 /* RKManagedObjectSearchEngine.m */; }; + 25B6E9ED14CF940700B1E881 /* RKSearchableManagedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E314CF940600B1E881 /* RKSearchableManagedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9EE14CF940700B1E881 /* RKSearchableManagedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E314CF940600B1E881 /* RKSearchableManagedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9EF14CF940700B1E881 /* RKSearchableManagedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E414CF940600B1E881 /* RKSearchableManagedObject.m */; }; + 25B6E9F014CF940700B1E881 /* RKSearchableManagedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E414CF940600B1E881 /* RKSearchableManagedObject.m */; }; + 25B6E9F114CF940700B1E881 /* RKSearchWord.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E514CF940600B1E881 /* RKSearchWord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9F214CF940700B1E881 /* RKSearchWord.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E514CF940600B1E881 /* RKSearchWord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9F314CF940700B1E881 /* RKSearchWord.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E614CF940600B1E881 /* RKSearchWord.m */; }; + 25B6E9F414CF940700B1E881 /* RKSearchWord.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E614CF940600B1E881 /* RKSearchWord.m */; }; + 25B6E9F514CF940700B1E881 /* RKSearchWordObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E714CF940700B1E881 /* RKSearchWordObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9F614CF940700B1E881 /* RKSearchWordObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9E714CF940700B1E881 /* RKSearchWordObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9F714CF940700B1E881 /* RKSearchWordObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E814CF940700B1E881 /* RKSearchWordObserver.m */; }; + 25B6E9F814CF940700B1E881 /* RKSearchWordObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9E814CF940700B1E881 /* RKSearchWordObserver.m */; }; + 25B6E9FD14CF943E00B1E881 /* RKCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9F914CF943D00B1E881 /* RKCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9FE14CF943E00B1E881 /* RKCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9F914CF943D00B1E881 /* RKCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6E9FF14CF943E00B1E881 /* RKCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9FA14CF943E00B1E881 /* RKCache.m */; }; + 25B6EA0014CF943E00B1E881 /* RKCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9FA14CF943E00B1E881 /* RKCache.m */; }; + 25B6EA0114CF943E00B1E881 /* RKMutableBlockDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9FB14CF943E00B1E881 /* RKMutableBlockDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6EA0214CF943E00B1E881 /* RKMutableBlockDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 25B6E9FB14CF943E00B1E881 /* RKMutableBlockDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25B6EA0314CF943E00B1E881 /* RKMutableBlockDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 25B6E9FC14CF943E00B1E881 /* RKMutableBlockDictionary.m */; }; + 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 */; }; + 25CAAA9415254E7800CAE5D7 /* ArrayOfHumans.json in Resources */ = {isa = PBXBuildFile; fileRef = 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */; }; + 25CAAA9515254E7800CAE5D7 /* ArrayOfHumans.json in Resources */ = {isa = PBXBuildFile; fileRef = 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */; }; + 25DB7508151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */; }; + 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 */; }; + 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 */; }; + 25EC1A3C14F72B1400C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF3914CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.m */; }; + 25EC1A3D14F72B2800C3CF3F /* RKInMemoryManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3C14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A3E14F72B2900C3CF3F /* RKInMemoryManagedObjectCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7394DF3C14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A3F14F72B3100C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF3D14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.m */; }; + 25EC1A4014F72B3300C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7394DF3D14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.m */; }; + 25EC1A4114F72C7200C3CF3F /* RKObjectMappingProvider+CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 73DA8E1A14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A4214F72C7300C3CF3F /* RKObjectMappingProvider+CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 73DA8E1A14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1A4314F72D0D00C3CF3F /* RKObjectMappingProvider+CoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 73DA8E1B14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.m */; }; + 25EC1A4514F7393D00C3CF3F /* RKObjectMappingProviderContextEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 73DA8E2114D1C3870054DD73 /* RKObjectMappingProviderContextEntry.m */; }; + 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, ); }; }; + 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 */; }; + 25EC1ABF14F8019F00C3CF3F /* RKRefreshGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1AB914F8019F00C3CF3F /* RKRefreshGestureRecognizer.m */; }; + 25EC1AC014F8019F00C3CF3F /* RKRefreshTriggerView.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1ABA14F8019F00C3CF3F /* RKRefreshTriggerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1AC114F8019F00C3CF3F /* RKRefreshTriggerView.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1ABA14F8019F00C3CF3F /* RKRefreshTriggerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1AC214F8019F00C3CF3F /* RKRefreshTriggerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1ABB14F8019F00C3CF3F /* RKRefreshTriggerView.m */; }; + 25EC1AC314F8019F00C3CF3F /* RKRefreshTriggerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1ABB14F8019F00C3CF3F /* RKRefreshTriggerView.m */; }; + 25EC1B3914F84B5D00C3CF3F /* UIImage+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1B3714F84B5C00C3CF3F /* UIImage+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1B3A14F84B5D00C3CF3F /* UIImage+RKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25EC1B3714F84B5C00C3CF3F /* UIImage+RKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25EC1B3B14F84B5D00C3CF3F /* UIImage+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1B3814F84B5C00C3CF3F /* UIImage+RKAdditions.m */; }; + 25EC1B3C14F84B5D00C3CF3F /* UIImage+RKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25EC1B3814F84B5C00C3CF3F /* UIImage+RKAdditions.m */; }; + 25FABED014E3796400E609E7 /* RKTestNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFAFD14D8EB30004863C8 /* RKTestNotificationObserver.m */; }; + 25FABED114E3796400E609E7 /* RKTestNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 252EFAFD14D8EB30004863C8 /* RKTestNotificationObserver.m */; }; + 25FABED214E3796B00E609E7 /* RKTestNotificationObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFAFC14D8EB30004863C8 /* RKTestNotificationObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25FABED314E3796C00E609E7 /* RKTestNotificationObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 252EFAFC14D8EB30004863C8 /* RKTestNotificationObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25FABED614E37A2B00E609E7 /* RKTestResponseLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 25FABED414E37A2B00E609E7 /* RKTestResponseLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25FABED714E37A2B00E609E7 /* RKTestResponseLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 25FABED414E37A2B00E609E7 /* RKTestResponseLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25FABED814E37A2B00E609E7 /* RKTestResponseLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FABED514E37A2B00E609E7 /* RKTestResponseLoader.m */; }; + 25FABED914E37A2B00E609E7 /* RKTestResponseLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FABED514E37A2B00E609E7 /* RKTestResponseLoader.m */; }; + 49A66B0C14CEFB0400A6F062 /* XMLReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A66B0914CEFB0400A6F062 /* XMLReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49A66B0D14CEFB0400A6F062 /* XMLReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A66B0914CEFB0400A6F062 /* XMLReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49A66B0E14CEFB0400A6F062 /* XMLReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A66B0A14CEFB0400A6F062 /* XMLReader.m */; }; + 49A66B0F14CEFB0400A6F062 /* XMLReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A66B0A14CEFB0400A6F062 /* XMLReader.m */; }; + 49A66B1214CF03CA00A6F062 /* RKXMLParserXMLReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A66B1014CF03CA00A6F062 /* RKXMLParserXMLReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49A66B1314CF03CA00A6F062 /* RKXMLParserXMLReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A66B1014CF03CA00A6F062 /* RKXMLParserXMLReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49A66B1414CF03CA00A6F062 /* RKXMLParserXMLReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A66B1114CF03CA00A6F062 /* RKXMLParserXMLReader.m */; }; + 49A66B1514CF03CA00A6F062 /* RKXMLParserXMLReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A66B1114CF03CA00A6F062 /* RKXMLParserXMLReader.m */; }; + 49D2759D14C9EF1E0090845D /* ISO8601DateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 49D2759914C9EF1E0090845D /* ISO8601DateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49D2759E14C9EF1E0090845D /* ISO8601DateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 49D2759914C9EF1E0090845D /* ISO8601DateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49D2759F14C9EF1E0090845D /* ISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 49D2759A14C9EF1E0090845D /* ISO8601DateFormatter.m */; }; + 49D275A114C9EF1E0090845D /* ISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 49D2759A14C9EF1E0090845D /* ISO8601DateFormatter.m */; }; + 49D275AD14C9F3020090845D /* RKISO8601DateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 49D275AB14C9F3020090845D /* RKISO8601DateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49D275AE14C9F3020090845D /* RKISO8601DateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 49D275AB14C9F3020090845D /* RKISO8601DateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 49D275AF14C9F3020090845D /* RKISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 49D275AC14C9F3020090845D /* RKISO8601DateFormatter.m */; }; + 49D275B014C9F3020090845D /* RKISO8601DateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 49D275AC14C9F3020090845D /* RKISO8601DateFormatter.m */; }; + 73D3907414CA1AE00093E3D6 /* parent.json in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907114CA19F90093E3D6 /* parent.json */; }; + 73D3907514CA1AE20093E3D6 /* parent.json in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907114CA19F90093E3D6 /* parent.json */; }; + 73D3907614CA1AE60093E3D6 /* child.json in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907314CA1A4A0093E3D6 /* child.json */; }; + 73D3907714CA1AE60093E3D6 /* child.json in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907314CA1A4A0093E3D6 /* child.json */; }; + 73D3907914CA1DD40093E3D6 /* channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907814CA1D710093E3D6 /* channels.xml */; }; + 73D3907A14CA1DD50093E3D6 /* channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 73D3907814CA1D710093E3D6 /* channels.xml */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -550,8 +775,23 @@ /* 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 = ""; }; + 25055B8314EEF32A00B9C4DD /* RKTestFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTestFactory.m; path = Testing/RKTestFactory.m; sourceTree = ""; }; + 25055B8D14EEF40000B9C4DD /* RKMappingTestExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKMappingTestExpectation.h; path = Testing/RKMappingTestExpectation.h; sourceTree = ""; }; + 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKMappingTestExpectation.m; path = Testing/RKMappingTestExpectation.m; sourceTree = ""; }; + 25079C6D151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSEntityDescription+RKAdditions.h"; sourceTree = ""; }; + 25079C6E151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSEntityDescription+RKAdditions.m"; sourceTree = ""; }; + 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSEntityDescription+RKAdditionsTest.m"; sourceTree = ""; }; 250CA67B147D8E800047D347 /* OCHamcrest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrest.framework; sourceTree = ""; }; 250CA67C147D8E800047D347 /* OCHamcrestIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrestIOS.framework; sourceTree = ""; }; + 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; }; 25160D2614564E820060A5C5 /* RestKitTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RestKitTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -560,7 +800,6 @@ 25160D46145650490060A5C5 /* CoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreData.h; sourceTree = ""; }; 25160D47145650490060A5C5 /* NSManagedObject+ActiveRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+ActiveRecord.h"; sourceTree = ""; }; 25160D48145650490060A5C5 /* NSManagedObject+ActiveRecord.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+ActiveRecord.m"; sourceTree = ""; }; - 25160D49145650490060A5C5 /* RKManagedObjectCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectCache.h; sourceTree = ""; }; 25160D4A145650490060A5C5 /* RKManagedObjectLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectLoader.h; sourceTree = ""; }; 25160D4B145650490060A5C5 /* RKManagedObjectLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectLoader.m; sourceTree = ""; }; 25160D4C145650490060A5C5 /* RKManagedObjectMapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectMapping.h; sourceTree = ""; }; @@ -576,12 +815,10 @@ 25160D56145650490060A5C5 /* RKObjectPropertyInspector+CoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RKObjectPropertyInspector+CoreData.h"; sourceTree = ""; }; 25160D57145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RKObjectPropertyInspector+CoreData.m"; sourceTree = ""; }; 25160D59145650490060A5C5 /* Network.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Network.h; sourceTree = ""; }; - 25160D5A145650490060A5C5 /* NSData+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+MD5.h"; path = "../Network/NSData+MD5.h"; sourceTree = ""; }; - 25160D5B145650490060A5C5 /* NSData+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+MD5.m"; path = "../Network/NSData+MD5.m"; sourceTree = ""; }; + 25160D5A145650490060A5C5 /* NSData+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSData+RKAdditions.h"; path = "../Network/NSData+RKAdditions.h"; sourceTree = ""; }; + 25160D5B145650490060A5C5 /* NSData+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSData+RKAdditions.m"; path = "../Network/NSData+RKAdditions.m"; sourceTree = ""; }; 25160D5C145650490060A5C5 /* NSDictionary+RKRequestSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+RKRequestSerialization.h"; sourceTree = ""; }; 25160D5D145650490060A5C5 /* NSDictionary+RKRequestSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RKRequestSerialization.m"; sourceTree = ""; }; - 25160D5E145650490060A5C5 /* NSString+MD5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+MD5.h"; path = "../Network/NSString+MD5.h"; sourceTree = ""; }; - 25160D5F145650490060A5C5 /* NSString+MD5.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSString+MD5.m"; path = "../Network/NSString+MD5.m"; sourceTree = ""; }; 25160D60145650490060A5C5 /* RKClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKClient.h; sourceTree = ""; }; 25160D61145650490060A5C5 /* RKClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKClient.m; sourceTree = ""; }; 25160D62145650490060A5C5 /* RKNotifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKNotifications.h; sourceTree = ""; }; @@ -647,27 +884,17 @@ 25160D9F145650490060A5C5 /* RKParserRegistry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParserRegistry.m; sourceTree = ""; }; 25160DA0145650490060A5C5 /* RKRouter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKRouter.h; sourceTree = ""; }; 25160DA1145650490060A5C5 /* RestKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestKit.h; sourceTree = ""; }; - 25160DA3145650490060A5C5 /* Errors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Errors.h; sourceTree = ""; }; - 25160DA4145650490060A5C5 /* Errors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Errors.m; sourceTree = ""; }; 25160DA5145650490060A5C5 /* lcl_config_components.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lcl_config_components.h; sourceTree = ""; }; 25160DA6145650490060A5C5 /* lcl_config_extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lcl_config_extensions.h; sourceTree = ""; }; 25160DA7145650490060A5C5 /* lcl_config_logger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lcl_config_logger.h; sourceTree = ""; }; 25160DA8145650490060A5C5 /* NSDictionary+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+RKAdditions.h"; sourceTree = ""; }; 25160DA9145650490060A5C5 /* NSDictionary+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RKAdditions.m"; sourceTree = ""; }; - 25160DAA145650490060A5C5 /* NSString+RestKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+RestKit.h"; sourceTree = ""; }; - 25160DAB145650490060A5C5 /* NSString+RestKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+RestKit.m"; sourceTree = ""; }; - 25160DAC145650490060A5C5 /* NSURL+RestKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+RestKit.h"; sourceTree = ""; }; - 25160DAD145650490060A5C5 /* NSURL+RestKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+RestKit.m"; sourceTree = ""; }; + 25160DAA145650490060A5C5 /* NSString+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+RKAdditions.h"; sourceTree = ""; }; + 25160DAB145650490060A5C5 /* NSString+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+RKAdditions.m"; sourceTree = ""; }; + 25160DAC145650490060A5C5 /* NSURL+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+RKAdditions.h"; sourceTree = ""; }; + 25160DAD145650490060A5C5 /* NSURL+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+RKAdditions.m"; sourceTree = ""; }; 25160DB0145650490060A5C5 /* RKJSONParserJSONKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKJSONParserJSONKit.h; sourceTree = ""; }; 25160DB1145650490060A5C5 /* RKJSONParserJSONKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserJSONKit.m; sourceTree = ""; }; - 25160DB2145650490060A5C5 /* RKJSONParserNXJSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKJSONParserNXJSON.h; sourceTree = ""; }; - 25160DB3145650490060A5C5 /* RKJSONParserNXJSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserNXJSON.m; sourceTree = ""; }; - 25160DB4145650490060A5C5 /* RKJSONParserSBJSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKJSONParserSBJSON.h; sourceTree = ""; }; - 25160DB5145650490060A5C5 /* RKJSONParserSBJSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserSBJSON.m; sourceTree = ""; }; - 25160DB6145650490060A5C5 /* RKJSONParserYAJL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKJSONParserYAJL.h; sourceTree = ""; }; - 25160DB7145650490060A5C5 /* RKJSONParserYAJL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserYAJL.m; sourceTree = ""; }; - 25160DB9145650490060A5C5 /* RKXMLParserLibXML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKXMLParserLibXML.h; sourceTree = ""; }; - 25160DBA145650490060A5C5 /* RKXMLParserLibXML.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKXMLParserLibXML.m; sourceTree = ""; }; 25160DBB145650490060A5C5 /* RestKit-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RestKit-Prefix.pch"; sourceTree = ""; }; 25160DBC145650490060A5C5 /* RKAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKAlert.h; sourceTree = ""; }; 25160DBD145650490060A5C5 /* RKAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKAlert.m; sourceTree = ""; }; @@ -720,12 +947,11 @@ 25160F7B145657220060A5C5 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; 25160F7D1456572F0060A5C5 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; 25160FA0145658BC0060A5C5 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = SDKs/MacOSX10.7.sdk/usr/lib/libxml2.dylib; sourceTree = DEVELOPER_DIR; }; - 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectLoaderSpec.m; sourceTree = ""; }; - 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectMappingOperationSpec.m; sourceTree = ""; }; - 25160FC91456F2330060A5C5 /* RKManagedObjectMappingSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectMappingSpec.m; sourceTree = ""; }; - 25160FCA1456F2330060A5C5 /* RKManagedObjectSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectSpec.m; sourceTree = ""; }; - 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectStoreSpec.m; sourceTree = ""; }; - 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectThreadSafeInvocationSpec.m; sourceTree = ""; }; + 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectLoaderTest.m; sourceTree = ""; }; + 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectMappingOperationTest.m; sourceTree = ""; }; + 25160FC91456F2330060A5C5 /* RKManagedObjectMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectMappingTest.m; sourceTree = ""; }; + 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectStoreTest.m; sourceTree = ""; }; + 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectThreadSafeInvocationTest.m; sourceTree = ""; }; 25160FCF1456F2330060A5C5 /* blake.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blake.png; sourceTree = ""; }; 25160FD11456F2330060A5C5 /* ArrayOfNestedDictionaries.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfNestedDictionaries.json; sourceTree = ""; }; 25160FD21456F2330060A5C5 /* ArrayOfResults.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfResults.json; sourceTree = ""; }; @@ -771,105 +997,237 @@ 251610001456F2330060A5C5 /* RKMappableAssociation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMappableAssociation.m; sourceTree = ""; }; 251610011456F2330060A5C5 /* RKMappableObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKMappableObject.h; sourceTree = ""; }; 251610021456F2330060A5C5 /* RKMappableObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMappableObject.m; sourceTree = ""; }; - 251610031456F2330060A5C5 /* RKObjectLoaderSpecResultModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectLoaderSpecResultModel.h; sourceTree = ""; }; - 251610041456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectLoaderSpecResultModel.m; sourceTree = ""; }; - 251610051456F2330060A5C5 /* RKObjectMapperSpecModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMapperSpecModel.h; sourceTree = ""; }; - 251610061456F2330060A5C5 /* RKObjectMapperSpecModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMapperSpecModel.m; sourceTree = ""; }; + 251610031456F2330060A5C5 /* RKObjectLoaderTestResultModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectLoaderTestResultModel.h; sourceTree = ""; }; + 251610041456F2330060A5C5 /* RKObjectLoaderTestResultModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectLoaderTestResultModel.m; sourceTree = ""; }; + 251610051456F2330060A5C5 /* RKObjectMapperTestModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMapperTestModel.h; sourceTree = ""; }; + 251610061456F2330060A5C5 /* RKObjectMapperTestModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMapperTestModel.m; sourceTree = ""; }; 251610071456F2330060A5C5 /* RKParent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKParent.h; sourceTree = ""; }; 251610081456F2330060A5C5 /* RKParent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParent.m; sourceTree = ""; }; 251610091456F2330060A5C5 /* RKResident.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKResident.h; sourceTree = ""; }; 2516100A1456F2330060A5C5 /* RKResident.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResident.m; sourceTree = ""; }; - 251610101456F2330060A5C5 /* RKAuthenticationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKAuthenticationSpec.m; sourceTree = ""; }; - 251610111456F2330060A5C5 /* RKClientSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKClientSpec.m; sourceTree = ""; }; - 251610121456F2330060A5C5 /* RKOAuthClientSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKOAuthClientSpec.m; sourceTree = ""; }; - 251610131456F2330060A5C5 /* RKParamsAttachmentSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParamsAttachmentSpec.m; sourceTree = ""; }; - 251610141456F2330060A5C5 /* RKParamsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParamsSpec.m; sourceTree = ""; }; - 251610171456F2330060A5C5 /* RKRequestQueueSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestQueueSpec.m; sourceTree = ""; }; - 251610181456F2330060A5C5 /* RKRequestSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestSpec.m; sourceTree = ""; }; - 251610191456F2330060A5C5 /* RKResponseSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResponseSpec.m; sourceTree = ""; }; - 2516101A1456F2330060A5C5 /* RKURLSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKURLSpec.m; sourceTree = ""; }; - 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicObjectMappingSpec.m; sourceTree = ""; }; - 2516101D1456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RKObjectiveCPlusPlusSpec.mm; sourceTree = ""; }; - 2516101E1456F2330060A5C5 /* RKObjectLoaderSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectLoaderSpec.m; sourceTree = ""; }; - 2516101F1456F2330060A5C5 /* RKObjectManagerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectManagerSpec.m; sourceTree = ""; }; - 251610211456F2330060A5C5 /* RKObjectMappingNextGenSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingNextGenSpec.m; sourceTree = ""; }; - 251610221456F2330060A5C5 /* RKObjectMappingOperationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingOperationSpec.m; sourceTree = ""; }; - 251610231456F2330060A5C5 /* RKObjectMappingProviderSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingProviderSpec.m; sourceTree = ""; }; - 251610241456F2330060A5C5 /* RKObjectMappingResultSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingResultSpec.m; sourceTree = ""; }; - 251610251456F2330060A5C5 /* RKObjectRouterSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectRouterSpec.m; sourceTree = ""; }; - 251610261456F2330060A5C5 /* RKObjectSerializerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectSerializerSpec.m; sourceTree = ""; }; - 251610271456F2330060A5C5 /* RKParserRegistrySpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParserRegistrySpec.m; sourceTree = ""; }; - 251610351456F2330060A5C5 /* RKSpecEnvironment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSpecEnvironment.h; sourceTree = ""; }; - 251610361456F2330060A5C5 /* RKSpecEnvironment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSpecEnvironment.m; sourceTree = ""; }; - 251610371456F2330060A5C5 /* RKSpecResponseLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSpecResponseLoader.h; sourceTree = ""; }; - 251610381456F2330060A5C5 /* RKSpecResponseLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSpecResponseLoader.m; sourceTree = ""; }; - 251610391456F2330060A5C5 /* set_ip_address.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = set_ip_address.scpt; sourceTree = ""; }; + 251610101456F2330060A5C5 /* RKAuthenticationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKAuthenticationTest.m; sourceTree = ""; }; + 251610111456F2330060A5C5 /* RKClientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKClientTest.m; sourceTree = ""; }; + 251610121456F2330060A5C5 /* RKOAuthClientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKOAuthClientTest.m; sourceTree = ""; }; + 251610131456F2330060A5C5 /* RKParamsAttachmentTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParamsAttachmentTest.m; sourceTree = ""; }; + 251610141456F2330060A5C5 /* RKParamsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParamsTest.m; sourceTree = ""; }; + 251610171456F2330060A5C5 /* RKRequestQueueTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestQueueTest.m; sourceTree = ""; }; + 251610181456F2330060A5C5 /* RKRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRequestTest.m; sourceTree = ""; }; + 251610191456F2330060A5C5 /* RKResponseTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResponseTest.m; sourceTree = ""; }; + 2516101A1456F2330060A5C5 /* RKURLTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKURLTest.m; sourceTree = ""; }; + 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicObjectMappingTest.m; sourceTree = ""; }; + 2516101E1456F2330060A5C5 /* RKObjectLoaderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectLoaderTest.m; sourceTree = ""; }; + 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectManagerTest.m; sourceTree = ""; }; + 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingNextGenTest.m; sourceTree = ""; }; + 251610221456F2330060A5C5 /* RKObjectMappingOperationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingOperationTest.m; sourceTree = ""; }; + 251610231456F2330060A5C5 /* RKObjectMappingProviderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingProviderTest.m; sourceTree = ""; }; + 251610241456F2330060A5C5 /* RKObjectMappingResultTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingResultTest.m; sourceTree = ""; }; + 251610251456F2330060A5C5 /* RKObjectRouterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectRouterTest.m; sourceTree = ""; }; + 251610261456F2330060A5C5 /* RKObjectSerializerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectSerializerTest.m; sourceTree = ""; }; + 251610271456F2330060A5C5 /* RKParserRegistryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParserRegistryTest.m; sourceTree = ""; }; + 251610351456F2330060A5C5 /* RKTestEnvironment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestEnvironment.h; sourceTree = ""; }; + 251610361456F2330060A5C5 /* RKTestEnvironment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestEnvironment.m; sourceTree = ""; }; 2516104B1456F2330060A5C5 /* authentication.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = authentication.rb; sourceTree = ""; }; 2516104C1456F2330060A5C5 /* etags.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = etags.rb; sourceTree = ""; }; 2516104D1456F2330060A5C5 /* oauth2.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = oauth2.rb; sourceTree = ""; }; 2516104E1456F2330060A5C5 /* timeout.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = timeout.rb; sourceTree = ""; }; 2516104F1456F2330060A5C5 /* restkit.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = restkit.rb; sourceTree = ""; }; 251610501456F2330060A5C5 /* server.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = server.rb; sourceTree = ""; }; - 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RKRequestSerializationSpec.m"; sourceTree = ""; }; - 251610531456F2330060A5C5 /* NSStringRestKitSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSStringRestKitSpec.m; sourceTree = ""; }; - 251610541456F2330060A5C5 /* RKDotNetDateFormatterSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDotNetDateFormatterSpec.m; sourceTree = ""; }; - 251610551456F2330060A5C5 /* RKJSONParserJSONKitSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserJSONKitSpec.m; sourceTree = ""; }; - 251610561456F2330060A5C5 /* RKPathMatcherSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKPathMatcherSpec.m; sourceTree = ""; }; - 251610571456F2330060A5C5 /* RKXMLParserSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKXMLParserSpec.m; sourceTree = ""; }; + 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RKRequestSerializationTest.m"; sourceTree = ""; }; + 251610531456F2330060A5C5 /* NSStringRestKitTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSStringRestKitTest.m; sourceTree = ""; }; + 251610541456F2330060A5C5 /* RKDotNetDateFormatterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDotNetDateFormatterTest.m; sourceTree = ""; }; + 251610551456F2330060A5C5 /* RKJSONParserJSONKitTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKJSONParserJSONKitTest.m; sourceTree = ""; }; + 251610561456F2330060A5C5 /* RKPathMatcherTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKPathMatcherTest.m; sourceTree = ""; }; + 251610571456F2330060A5C5 /* RKXMLParserTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKXMLParserTest.m; sourceTree = ""; }; 251611281456F50F0060A5C5 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 2516112A1456F5170060A5C5 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 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; }; - 25A34192147C2F370009758D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 25A34193147C2F370009758D /* NSInvocation+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+OCMAdditions.h"; sourceTree = ""; }; - 25A34194147C2F370009758D /* NSInvocation+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+OCMAdditions.m"; sourceTree = ""; }; - 25A34195147C2F370009758D /* NSMethodSignature+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+OCMAdditions.h"; sourceTree = ""; }; - 25A34196147C2F370009758D /* NSMethodSignature+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+OCMAdditions.m"; sourceTree = ""; }; - 25A34197147C2F370009758D /* NSNotificationCenter+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNotificationCenter+OCMAdditions.h"; sourceTree = ""; }; - 25A34198147C2F370009758D /* NSNotificationCenter+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OCMAdditions.m"; sourceTree = ""; }; - 25A34199147C2F370009758D /* OCClassMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCClassMockObject.h; sourceTree = ""; }; - 25A3419A147C2F370009758D /* OCClassMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCClassMockObject.m; sourceTree = ""; }; - 25A3419B147C2F370009758D /* OCMArg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMArg.h; sourceTree = ""; }; - 25A3419C147C2F370009758D /* OCMArg.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArg.m; sourceTree = ""; }; - 25A3419D147C2F370009758D /* OCMBlockCaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBlockCaller.h; sourceTree = ""; }; - 25A3419E147C2F370009758D /* OCMBlockCaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBlockCaller.m; sourceTree = ""; }; - 25A3419F147C2F370009758D /* OCMBoxedReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBoxedReturnValueProvider.h; sourceTree = ""; }; - 25A341A0147C2F370009758D /* OCMBoxedReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProvider.m; sourceTree = ""; }; - 25A341A1147C2F370009758D /* OCMConstraint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMConstraint.h; sourceTree = ""; }; - 25A341A2147C2F370009758D /* OCMConstraint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMConstraint.m; sourceTree = ""; }; - 25A341A3147C2F370009758D /* OCMExceptionReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMExceptionReturnValueProvider.h; sourceTree = ""; }; - 25A341A4147C2F370009758D /* OCMExceptionReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMExceptionReturnValueProvider.m; sourceTree = ""; }; - 25A341A5147C2F370009758D /* OCMIndirectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMIndirectReturnValueProvider.h; sourceTree = ""; }; - 25A341A6147C2F370009758D /* OCMIndirectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIndirectReturnValueProvider.m; sourceTree = ""; }; - 25A341A7147C2F370009758D /* OCMNotificationPoster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMNotificationPoster.h; sourceTree = ""; }; - 25A341A8147C2F370009758D /* OCMNotificationPoster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNotificationPoster.m; sourceTree = ""; }; - 25A341A9147C2F370009758D /* OCMObserverRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMObserverRecorder.h; sourceTree = ""; }; - 25A341AA147C2F370009758D /* OCMObserverRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMObserverRecorder.m; sourceTree = ""; }; - 25A341AB147C2F370009758D /* OCMock-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "OCMock-Info.plist"; sourceTree = ""; }; - 25A341AC147C2F370009758D /* OCMock-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMock-Prefix.pch"; sourceTree = ""; }; - 25A341AD147C2F370009758D /* OCMock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMock.h; sourceTree = ""; }; - 25A341AE147C2F370009758D /* OCMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockObject.h; sourceTree = ""; }; - 25A341AF147C2F370009758D /* OCMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObject.m; sourceTree = ""; }; - 25A341B0147C2F370009758D /* OCMockRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockRecorder.h; sourceTree = ""; }; - 25A341B1147C2F370009758D /* OCMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockRecorder.m; sourceTree = ""; }; - 25A341B2147C2F370009758D /* OCMPassByRefSetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMPassByRefSetter.h; sourceTree = ""; }; - 25A341B3147C2F370009758D /* OCMPassByRefSetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMPassByRefSetter.m; sourceTree = ""; }; - 25A341B4147C2F370009758D /* OCMRealObjectForwarder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRealObjectForwarder.h; sourceTree = ""; }; - 25A341B5147C2F370009758D /* OCMRealObjectForwarder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMRealObjectForwarder.m; sourceTree = ""; }; - 25A341B6147C2F370009758D /* OCMReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMReturnValueProvider.h; sourceTree = ""; }; - 25A341B7147C2F370009758D /* OCMReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMReturnValueProvider.m; sourceTree = ""; }; - 25A341B8147C2F370009758D /* OCObserverMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCObserverMockObject.h; sourceTree = ""; }; - 25A341B9147C2F370009758D /* OCObserverMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCObserverMockObject.m; sourceTree = ""; }; - 25A341BA147C2F370009758D /* OCPartialMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockObject.h; sourceTree = ""; }; - 25A341BB147C2F370009758D /* OCPartialMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockObject.m; sourceTree = ""; }; - 25A341BC147C2F370009758D /* OCPartialMockRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockRecorder.h; sourceTree = ""; }; - 25A341BD147C2F370009758D /* OCPartialMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockRecorder.m; sourceTree = ""; }; - 25A341BE147C2F370009758D /* OCProtocolMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCProtocolMockObject.h; sourceTree = ""; }; - 25A341BF147C2F370009758D /* OCProtocolMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCProtocolMockObject.m; sourceTree = ""; }; + 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 = ""; }; + 252EFAFD14D8EB30004863C8 /* RKTestNotificationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTestNotificationObserver.m; path = Testing/RKTestNotificationObserver.m; sourceTree = ""; }; + 252EFB0614D98F4D004863C8 /* RKSearchableManagedObjectTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchableManagedObjectTest.m; sourceTree = ""; }; + 252EFB0714D98F4D004863C8 /* RKSearchWordObserverTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchWordObserverTest.m; sourceTree = ""; }; + 252EFB0C14D98F76004863C8 /* RKMutableBlockDictionaryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMutableBlockDictionaryTest.m; sourceTree = ""; }; + 252EFB1914D9A7CB004863C8 /* NSBundle+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+RKAdditions.h"; sourceTree = ""; }; + 252EFB1A14D9A7CB004863C8 /* NSBundle+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+RKAdditions.m"; sourceTree = ""; }; + 252EFB2014D9B35D004863C8 /* RKTestFixture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKTestFixture.h; path = Testing/RKTestFixture.h; sourceTree = ""; }; + 252EFB2114D9B35D004863C8 /* RKTestFixture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKTestFixture.m; path = Testing/RKTestFixture.m; sourceTree = ""; }; + 252EFB2414D9B6F2004863C8 /* Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Testing.h; path = Testing/Testing.h; sourceTree = ""; }; + 252EFB2714DA0689004863C8 /* NakedEvents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = NakedEvents.json; sourceTree = ""; }; + 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 = ""; }; + 25B6E90314CF778D00B1E881 /* RKAbstractTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKAbstractTableController.h; sourceTree = ""; }; + 25B6E90414CF778D00B1E881 /* RKAbstractTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKAbstractTableController.m; sourceTree = ""; }; + 25B6E90614CF778D00B1E881 /* RKAbstractTableController_Internals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKAbstractTableController_Internals.h; sourceTree = ""; }; + 25B6E90714CF778D00B1E881 /* RKControlTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKControlTableItem.h; sourceTree = ""; }; + 25B6E90814CF778D00B1E881 /* RKControlTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKControlTableItem.m; sourceTree = ""; }; + 25B6E90914CF778D00B1E881 /* RKControlTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKControlTableViewCell.h; sourceTree = ""; }; + 25B6E90A14CF778D00B1E881 /* RKControlTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKControlTableViewCell.m; sourceTree = ""; }; + 25B6E90B14CF778D00B1E881 /* RKFetchedResultsTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKFetchedResultsTableController.h; sourceTree = ""; }; + 25B6E90C14CF778D00B1E881 /* RKFetchedResultsTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFetchedResultsTableController.m; sourceTree = ""; }; + 25B6E90D14CF778D00B1E881 /* RKForm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKForm.h; sourceTree = ""; }; + 25B6E90E14CF778D00B1E881 /* RKForm.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKForm.m; sourceTree = ""; }; + 25B6E90F14CF778D00B1E881 /* RKFormSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKFormSection.h; sourceTree = ""; }; + 25B6E91014CF778D00B1E881 /* RKFormSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFormSection.m; sourceTree = ""; }; + 25B6E91114CF778D00B1E881 /* RKTableController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTableController.h; sourceTree = ""; }; + 25B6E91214CF778D00B1E881 /* RKTableController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableController.m; sourceTree = ""; }; + 25B6E91314CF778D00B1E881 /* RKTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTableItem.h; sourceTree = ""; }; + 25B6E91414CF778D00B1E881 /* RKTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableItem.m; sourceTree = ""; }; + 25B6E91514CF778D00B1E881 /* RKTableSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTableSection.h; sourceTree = ""; }; + 25B6E91614CF778D00B1E881 /* RKTableSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableSection.m; sourceTree = ""; }; + 25B6E91714CF778D00B1E881 /* RKTableViewCellMapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTableViewCellMapping.h; sourceTree = ""; }; + 25B6E91814CF778D00B1E881 /* RKTableViewCellMapping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableViewCellMapping.m; sourceTree = ""; }; + 25B6E91914CF778D00B1E881 /* RKTableViewCellMappings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTableViewCellMappings.h; sourceTree = ""; }; + 25B6E91A14CF778D00B1E881 /* RKTableViewCellMappings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableViewCellMappings.m; sourceTree = ""; }; + 25B6E91B14CF778D00B1E881 /* UI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UI.h; sourceTree = ""; }; + 25B6E91C14CF778D00B1E881 /* UIView+FindFirstResponder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+FindFirstResponder.h"; sourceTree = ""; }; + 25B6E91D14CF778D00B1E881 /* UIView+FindFirstResponder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+FindFirstResponder.m"; sourceTree = ""; }; + 25B6E95414CF795D00B1E881 /* RKErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKErrors.h; sourceTree = ""; }; + 25B6E95714CF7A1C00B1E881 /* RKErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKErrors.m; sourceTree = ""; }; + 25B6E95A14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKDynamicObjectMappingMatcher.h; sourceTree = ""; }; + 25B6E95B14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicObjectMappingMatcher.m; sourceTree = ""; }; + 25B6E97C14CF829400B1E881 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 25B6E97D14CF829400B1E881 /* NSInvocation+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+OCMAdditions.h"; sourceTree = ""; }; + 25B6E97E14CF829400B1E881 /* NSInvocation+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+OCMAdditions.m"; sourceTree = ""; }; + 25B6E97F14CF829400B1E881 /* NSMethodSignature+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+OCMAdditions.h"; sourceTree = ""; }; + 25B6E98014CF829400B1E881 /* NSMethodSignature+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+OCMAdditions.m"; sourceTree = ""; }; + 25B6E98114CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OCMAdditions.m"; sourceTree = ""; }; + 25B6E98214CF829400B1E881 /* OCClassMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCClassMockObject.h; sourceTree = ""; }; + 25B6E98314CF829400B1E881 /* OCClassMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCClassMockObject.m; sourceTree = ""; }; + 25B6E98414CF829400B1E881 /* OCMArg.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArg.m; sourceTree = ""; }; + 25B6E98514CF829400B1E881 /* OCMBlockCaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBlockCaller.h; sourceTree = ""; }; + 25B6E98614CF829400B1E881 /* OCMBlockCaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBlockCaller.m; sourceTree = ""; }; + 25B6E98714CF829400B1E881 /* OCMBoxedReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBoxedReturnValueProvider.h; sourceTree = ""; }; + 25B6E98814CF829400B1E881 /* OCMBoxedReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProvider.m; sourceTree = ""; }; + 25B6E98914CF829400B1E881 /* OCMConstraint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMConstraint.m; sourceTree = ""; }; + 25B6E98A14CF829400B1E881 /* OCMExceptionReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMExceptionReturnValueProvider.h; sourceTree = ""; }; + 25B6E98B14CF829400B1E881 /* OCMExceptionReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMExceptionReturnValueProvider.m; sourceTree = ""; }; + 25B6E98C14CF829400B1E881 /* OCMIndirectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMIndirectReturnValueProvider.h; sourceTree = ""; }; + 25B6E98D14CF829400B1E881 /* OCMIndirectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIndirectReturnValueProvider.m; sourceTree = ""; }; + 25B6E98E14CF829400B1E881 /* OCMNotificationPoster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMNotificationPoster.h; sourceTree = ""; }; + 25B6E98F14CF829400B1E881 /* OCMNotificationPoster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNotificationPoster.m; sourceTree = ""; }; + 25B6E99014CF829400B1E881 /* OCMObserverRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMObserverRecorder.h; sourceTree = ""; }; + 25B6E99114CF829400B1E881 /* OCMObserverRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMObserverRecorder.m; sourceTree = ""; }; + 25B6E99214CF829400B1E881 /* OCMock-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "OCMock-Info.plist"; sourceTree = ""; }; + 25B6E99314CF829400B1E881 /* OCMock-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMock-Prefix.pch"; sourceTree = ""; }; + 25B6E99414CF829400B1E881 /* OCMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObject.m; sourceTree = ""; }; + 25B6E99514CF829400B1E881 /* OCMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockRecorder.m; sourceTree = ""; }; + 25B6E99614CF829400B1E881 /* OCMPassByRefSetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMPassByRefSetter.h; sourceTree = ""; }; + 25B6E99714CF829400B1E881 /* OCMPassByRefSetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMPassByRefSetter.m; sourceTree = ""; }; + 25B6E99814CF829400B1E881 /* OCMRealObjectForwarder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRealObjectForwarder.h; sourceTree = ""; }; + 25B6E99914CF829400B1E881 /* OCMRealObjectForwarder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMRealObjectForwarder.m; sourceTree = ""; }; + 25B6E99A14CF829400B1E881 /* OCMReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMReturnValueProvider.h; sourceTree = ""; }; + 25B6E99B14CF829400B1E881 /* OCMReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMReturnValueProvider.m; sourceTree = ""; }; + 25B6E99C14CF829400B1E881 /* OCObserverMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCObserverMockObject.h; sourceTree = ""; }; + 25B6E99D14CF829400B1E881 /* OCObserverMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCObserverMockObject.m; sourceTree = ""; }; + 25B6E99E14CF829400B1E881 /* OCPartialMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockObject.h; sourceTree = ""; }; + 25B6E99F14CF829400B1E881 /* OCPartialMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockObject.m; sourceTree = ""; }; + 25B6E9A014CF829400B1E881 /* OCPartialMockRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockRecorder.h; sourceTree = ""; }; + 25B6E9A114CF829400B1E881 /* OCPartialMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockRecorder.m; sourceTree = ""; }; + 25B6E9A214CF829400B1E881 /* OCProtocolMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCProtocolMockObject.h; sourceTree = ""; }; + 25B6E9A314CF829400B1E881 /* OCProtocolMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCProtocolMockObject.m; sourceTree = ""; }; + 25B6E9D514CF912500B1E881 /* RKSearchable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchable.h; sourceTree = ""; }; + 25B6E9D614CF912500B1E881 /* RKSearchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchable.m; sourceTree = ""; }; + 25B6E9D714CF912500B1E881 /* RKTestAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestAddress.h; sourceTree = ""; }; + 25B6E9D814CF912500B1E881 /* RKTestAddress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestAddress.m; sourceTree = ""; }; + 25B6E9D914CF912500B1E881 /* RKTestUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestUser.h; sourceTree = ""; }; + 25B6E9DA14CF912500B1E881 /* RKTestUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestUser.m; sourceTree = ""; }; + 25B6E9E114CF940600B1E881 /* RKManagedObjectSearchEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKManagedObjectSearchEngine.h; sourceTree = ""; }; + 25B6E9E214CF940600B1E881 /* RKManagedObjectSearchEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectSearchEngine.m; sourceTree = ""; }; + 25B6E9E314CF940600B1E881 /* RKSearchableManagedObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchableManagedObject.h; sourceTree = ""; }; + 25B6E9E414CF940600B1E881 /* RKSearchableManagedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchableManagedObject.m; sourceTree = ""; }; + 25B6E9E514CF940600B1E881 /* RKSearchWord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchWord.h; sourceTree = ""; }; + 25B6E9E614CF940600B1E881 /* RKSearchWord.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchWord.m; sourceTree = ""; }; + 25B6E9E714CF940700B1E881 /* RKSearchWordObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchWordObserver.h; sourceTree = ""; }; + 25B6E9E814CF940700B1E881 /* RKSearchWordObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchWordObserver.m; sourceTree = ""; }; + 25B6E9F914CF943D00B1E881 /* RKCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKCache.h; sourceTree = ""; }; + 25B6E9FA14CF943E00B1E881 /* RKCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKCache.m; sourceTree = ""; }; + 25B6E9FB14CF943E00B1E881 /* RKMutableBlockDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKMutableBlockDictionary.h; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 25EC1ABB14F8019F00C3CF3F /* RKRefreshTriggerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKRefreshTriggerView.m; sourceTree = ""; }; + 25EC1AD814F8022600C3CF3F /* RestKitFramework-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RestKitFramework-Info.plist"; sourceTree = ""; }; + 25EC1AD914F8022600C3CF3F /* RestKitFrameworkTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RestKitFrameworkTests-Info.plist"; sourceTree = ""; }; + 25EC1ADA14F8022600C3CF3F /* RestKitTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RestKitTests-Info.plist"; sourceTree = ""; }; + 25EC1ADC14F8022600C3CF3F /* RestKitCoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RestKitCoreData.xcdatamodel; sourceTree = ""; }; + 25EC1ADE14F8022600C3CF3F /* blackArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blackArrow.png; sourceTree = ""; }; + 25EC1ADF14F8022600C3CF3F /* blackArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "blackArrow@2x.png"; sourceTree = ""; }; + 25EC1AE014F8022600C3CF3F /* blueArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blueArrow.png; sourceTree = ""; }; + 25EC1AE114F8022600C3CF3F /* blueArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "blueArrow@2x.png"; sourceTree = ""; }; + 25EC1AE214F8022600C3CF3F /* grayArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = grayArrow.png; sourceTree = ""; }; + 25EC1AE314F8022600C3CF3F /* grayArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "grayArrow@2x.png"; sourceTree = ""; }; + 25EC1AE414F8022600C3CF3F /* whiteArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = whiteArrow.png; sourceTree = ""; }; + 25EC1AE514F8022600C3CF3F /* whiteArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "whiteArrow@2x.png"; sourceTree = ""; }; + 25EC1B0014F8078100C3CF3F /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + 25EC1B1E14F821B500C3CF3F /* RestKitResources-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RestKitResources-Prefix.pch"; sourceTree = ""; }; + 25EC1B1F14F8220800C3CF3F /* RestKitResources-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RestKitResources-Info.plist"; sourceTree = ""; }; + 25EC1B3714F84B5C00C3CF3F /* UIImage+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+RKAdditions.h"; sourceTree = ""; }; + 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 = ""; }; + 49A66B1014CF03CA00A6F062 /* RKXMLParserXMLReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKXMLParserXMLReader.h; sourceTree = ""; }; + 49A66B1114CF03CA00A6F062 /* RKXMLParserXMLReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKXMLParserXMLReader.m; sourceTree = ""; }; + 49D2759914C9EF1E0090845D /* ISO8601DateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ISO8601DateFormatter.h; path = iso8601parser/ISO8601DateFormatter.h; sourceTree = ""; }; + 49D2759A14C9EF1E0090845D /* ISO8601DateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ISO8601DateFormatter.m; path = iso8601parser/ISO8601DateFormatter.m; sourceTree = ""; }; + 49D2759B14C9EF1E0090845D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE.txt; path = iso8601parser/LICENSE.txt; sourceTree = ""; }; + 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 /* 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 = ""; }; + 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 = ""; }; + 73DA8E1A14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RKObjectMappingProvider+CoreData.h"; sourceTree = ""; }; + 73DA8E1B14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RKObjectMappingProvider+CoreData.m"; sourceTree = ""; }; + 73DA8E2014D1C3870054DD73 /* RKObjectMappingProviderContextEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMappingProviderContextEntry.h; sourceTree = ""; }; + 73DA8E2114D1C3870054DD73 /* RKObjectMappingProviderContextEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappingProviderContextEntry.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -886,13 +1244,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 25055B9314EEFEC800B9C4DD /* libRestKit.a in Frameworks */, + 25B6EA0814CF947E00B1E881 /* CoreGraphics.framework in Frameworks */, + 25B6EA0614CF946400B1E881 /* QuartzCore.framework in Frameworks */, 251611321456F56C0060A5C5 /* MobileCoreServices.framework in Frameworks */, 251611301456F5590060A5C5 /* Security.framework in Frameworks */, 2516112E1456F5520060A5C5 /* CoreData.framework in Frameworks */, 2516112C1456F51D0060A5C5 /* libxml2.dylib in Frameworks */, 2516112B1456F5170060A5C5 /* CFNetwork.framework in Frameworks */, 251611291456F50F0060A5C5 /* SystemConfiguration.framework in Frameworks */, - 251611271456F4A90060A5C5 /* libRestKit.a in Frameworks */, 25160D2814564E820060A5C5 /* SenTestingKit.framework in Frameworks */, 25160D2A14564E820060A5C5 /* UIKit.framework in Frameworks */, 25160D2B14564E820060A5C5 /* Foundation.framework in Frameworks */, @@ -922,6 +1282,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 259C301315128079003066A2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 259C301715128079003066A2 /* CoreFoundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -938,7 +1306,8 @@ isa = PBXGroup; children = ( 25160D44145650490060A5C5 /* Code */, - 25160FC51456F2330060A5C5 /* Specs */, + 25160FC51456F2330060A5C5 /* Tests */, + 25EC1AD614F8022600C3CF3F /* Resources */, 25160E8D145652E40060A5C5 /* Vendor */, 25160D1814564E810060A5C5 /* Frameworks */, 25160D1714564E810060A5C5 /* Products */, @@ -952,6 +1321,7 @@ 25160D2614564E820060A5C5 /* RestKitTests.octest */, 25160E62145651060060A5C5 /* RestKit.framework */, 25160E78145651060060A5C5 /* RestKitFrameworkTests.octest */, + 259C301615128079003066A2 /* RestKitResources.bundle */, ); name = Products; sourceTree = ""; @@ -959,6 +1329,8 @@ 25160D1814564E810060A5C5 /* Frameworks */ = { isa = PBXGroup; children = ( + 25B6EA0714CF947D00B1E881 /* CoreGraphics.framework */, + 25B6EA0514CF946300B1E881 /* QuartzCore.framework */, 25A34244147D8AAA0009758D /* Security.framework */, 251611311456F56C0060A5C5 /* MobileCoreServices.framework */, 2516112F1456F5590060A5C5 /* Security.framework */, @@ -973,6 +1345,7 @@ 25160D2714564E820060A5C5 /* SenTestingKit.framework */, 25160D2914564E820060A5C5 /* UIKit.framework */, 25160E63145651060060A5C5 /* Cocoa.framework */, + 25EC1B0014F8078100C3CF3F /* CoreFoundation.framework */, 25160E65145651060060A5C5 /* Other Frameworks */, ); name = Frameworks; @@ -986,6 +1359,8 @@ 25160D7A145650490060A5C5 /* ObjectMapping */, 25160D45145650490060A5C5 /* CoreData */, 25160DA2145650490060A5C5 /* Support */, + 25B6E90214CF778D00B1E881 /* UI */, + 252EFB1F14D9A8D4004863C8 /* Testing */, ); path = Code; sourceTree = ""; @@ -993,14 +1368,26 @@ 25160D45145650490060A5C5 /* CoreData */ = { isa = PBXGroup; children = ( + 25B6E9E114CF940600B1E881 /* RKManagedObjectSearchEngine.h */, + 25B6E9E214CF940600B1E881 /* RKManagedObjectSearchEngine.m */, + 25B6E9E314CF940600B1E881 /* RKSearchableManagedObject.h */, + 25B6E9E414CF940600B1E881 /* RKSearchableManagedObject.m */, + 25B6E9E514CF940600B1E881 /* RKSearchWord.h */, + 25B6E9E614CF940600B1E881 /* RKSearchWord.m */, + 25B6E9E714CF940700B1E881 /* RKSearchWordObserver.h */, + 25B6E9E814CF940700B1E881 /* RKSearchWordObserver.m */, 25160D46145650490060A5C5 /* CoreData.h */, 25160D47145650490060A5C5 /* NSManagedObject+ActiveRecord.h */, 25160D48145650490060A5C5 /* NSManagedObject+ActiveRecord.m */, - 25160D49145650490060A5C5 /* RKManagedObjectCache.h */, + 7394DF3814CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.h */, + 7394DF3914CF168C00CE7BCE /* RKFetchRequestManagedObjectCache.m */, + 7394DF3C14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.h */, + 7394DF3D14CF19F200CE7BCE /* RKInMemoryManagedObjectCache.m */, 25160D4A145650490060A5C5 /* RKManagedObjectLoader.h */, 25160D4B145650490060A5C5 /* RKManagedObjectLoader.m */, 25160D4C145650490060A5C5 /* RKManagedObjectMapping.h */, 25160D4D145650490060A5C5 /* RKManagedObjectMapping.m */, + 7394DF3514CF157A00CE7BCE /* RKManagedObjectCaching.h */, 25160D4E145650490060A5C5 /* RKManagedObjectMappingOperation.h */, 25160D4F145650490060A5C5 /* RKManagedObjectMappingOperation.m */, 25160D50145650490060A5C5 /* RKManagedObjectSeeder.h */, @@ -1009,8 +1396,20 @@ 25160D53145650490060A5C5 /* RKManagedObjectStore.m */, 25160D54145650490060A5C5 /* RKManagedObjectThreadSafeInvocation.h */, 25160D55145650490060A5C5 /* RKManagedObjectThreadSafeInvocation.m */, + 73DA8E1A14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.h */, + 73DA8E1B14D1BA960054DD73 /* RKObjectMappingProvider+CoreData.m */, 25160D56145650490060A5C5 /* RKObjectPropertyInspector+CoreData.h */, 25160D57145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m */, + 257ABAAE15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.h */, + 257ABAAF15112DD400CCAA76 /* NSManagedObjectContext+RKAdditions.m */, + 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */, + 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 = ""; @@ -1054,7 +1453,10 @@ 25160D7A145650490060A5C5 /* ObjectMapping */ = { isa = PBXGroup; children = ( + 25B6E95A14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h */, + 25B6E95B14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m */, 25160D7B145650490060A5C5 /* ObjectMapping.h */, + 2513504D14B8FE6B00A7E893 /* RKConfigurationDelegate.h */, 25160D7C145650490060A5C5 /* RKDynamicObjectMapping.h */, 25160D7D145650490060A5C5 /* RKDynamicObjectMapping.m */, 25160D7E145650490060A5C5 /* RKErrorMessage.h */, @@ -1077,10 +1479,15 @@ 25160D8F145650490060A5C5 /* RKObjectMappingDefinition.h */, 25160D90145650490060A5C5 /* RKObjectMappingOperation.h */, 25160D91145650490060A5C5 /* RKObjectMappingOperation.m */, + 250DF25E14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h */, 25160D92145650490060A5C5 /* RKObjectMappingProvider.h */, 25160D93145650490060A5C5 /* RKObjectMappingProvider.m */, + 73DA8E2014D1C3870054DD73 /* RKObjectMappingProviderContextEntry.h */, + 73DA8E2114D1C3870054DD73 /* RKObjectMappingProviderContextEntry.m */, 25160D94145650490060A5C5 /* RKObjectMappingResult.h */, 25160D95145650490060A5C5 /* RKObjectMappingResult.m */, + 254A62B714AD544200939BEE /* RKObjectPaginator.h */, + 254A62B814AD544200939BEE /* RKObjectPaginator.m */, 25160D96145650490060A5C5 /* RKObjectPropertyInspector.h */, 25160D97145650490060A5C5 /* RKObjectPropertyInspector.m */, 25160D98145650490060A5C5 /* RKObjectRelationshipMapping.h */, @@ -1092,6 +1499,7 @@ 25160D9E145650490060A5C5 /* RKParserRegistry.h */, 25160D9F145650490060A5C5 /* RKParserRegistry.m */, 25160DA0145650490060A5C5 /* RKRouter.h */, + 25CA7A8E14EC570100888FF8 /* RKObjectMappingDefinition.m */, ); path = ObjectMapping; sourceTree = ""; @@ -1099,21 +1507,23 @@ 25160DA2145650490060A5C5 /* Support */ = { isa = PBXGroup; children = ( - 25160D5A145650490060A5C5 /* NSData+MD5.h */, - 25160D5B145650490060A5C5 /* NSData+MD5.m */, - 25160D5E145650490060A5C5 /* NSString+MD5.h */, - 25160D5F145650490060A5C5 /* NSString+MD5.m */, - 25160DA3145650490060A5C5 /* Errors.h */, - 25160DA4145650490060A5C5 /* Errors.m */, + 25545957155F0527007D7625 /* RKBenchmark.h */, + 25545958155F0527007D7625 /* RKBenchmark.m */, + 25B6E9F914CF943D00B1E881 /* RKCache.h */, + 25B6E9FA14CF943E00B1E881 /* RKCache.m */, + 25B6E9FB14CF943E00B1E881 /* RKMutableBlockDictionary.h */, + 25B6E9FC14CF943E00B1E881 /* RKMutableBlockDictionary.m */, + 25160D5A145650490060A5C5 /* NSData+RKAdditions.h */, + 25160D5B145650490060A5C5 /* NSData+RKAdditions.m */, 25160DA5145650490060A5C5 /* lcl_config_components.h */, 25160DA6145650490060A5C5 /* lcl_config_extensions.h */, 25160DA7145650490060A5C5 /* lcl_config_logger.h */, 25160DA8145650490060A5C5 /* NSDictionary+RKAdditions.h */, 25160DA9145650490060A5C5 /* NSDictionary+RKAdditions.m */, - 25160DAA145650490060A5C5 /* NSString+RestKit.h */, - 25160DAB145650490060A5C5 /* NSString+RestKit.m */, - 25160DAC145650490060A5C5 /* NSURL+RestKit.h */, - 25160DAD145650490060A5C5 /* NSURL+RestKit.m */, + 25160DAA145650490060A5C5 /* NSString+RKAdditions.h */, + 25160DAB145650490060A5C5 /* NSString+RKAdditions.m */, + 25160DAC145650490060A5C5 /* NSURL+RKAdditions.h */, + 25160DAD145650490060A5C5 /* NSURL+RKAdditions.m */, 25160DAE145650490060A5C5 /* Parsers */, 25160DBB145650490060A5C5 /* RestKit-Prefix.pch */, 25160DBC145650490060A5C5 /* RKAlert.h */, @@ -1121,6 +1531,8 @@ 25160DBE145650490060A5C5 /* RKDotNetDateFormatter.h */, 25160DBF145650490060A5C5 /* RKDotNetDateFormatter.m */, 25160DC0145650490060A5C5 /* RKFixCategoryBug.h */, + 49D275AB14C9F3020090845D /* RKISO8601DateFormatter.h */, + 49D275AC14C9F3020090845D /* RKISO8601DateFormatter.m */, 25160DC1145650490060A5C5 /* RKLog.h */, 25160DC2145650490060A5C5 /* RKLog.m */, 25160DC3145650490060A5C5 /* RKMIMETypes.h */, @@ -1133,6 +1545,16 @@ 25160DCA145650490060A5C5 /* Support.h */, 25B408241491CDDB00F21111 /* RKDirectory.h */, 25B408251491CDDB00F21111 /* RKDirectory.m */, + 250DF22814C5190E0001DEFA /* RKOrderedDictionary.h */, + 250DF22914C5190E0001DEFA /* RKOrderedDictionary.m */, + 25B6E95414CF795D00B1E881 /* RKErrors.h */, + 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 = ""; @@ -1151,12 +1573,6 @@ children = ( 25160DB0145650490060A5C5 /* RKJSONParserJSONKit.h */, 25160DB1145650490060A5C5 /* RKJSONParserJSONKit.m */, - 25160DB2145650490060A5C5 /* RKJSONParserNXJSON.h */, - 25160DB3145650490060A5C5 /* RKJSONParserNXJSON.m */, - 25160DB4145650490060A5C5 /* RKJSONParserSBJSON.h */, - 25160DB5145650490060A5C5 /* RKJSONParserSBJSON.m */, - 25160DB6145650490060A5C5 /* RKJSONParserYAJL.h */, - 25160DB7145650490060A5C5 /* RKJSONParserYAJL.m */, ); path = JSON; sourceTree = ""; @@ -1164,8 +1580,8 @@ 25160DB8145650490060A5C5 /* XML */ = { isa = PBXGroup; children = ( - 25160DB9145650490060A5C5 /* RKXMLParserLibXML.h */, - 25160DBA145650490060A5C5 /* RKXMLParserLibXML.m */, + 49A66B1014CF03CA00A6F062 /* RKXMLParserXMLReader.h */, + 49A66B1114CF03CA00A6F062 /* RKXMLParserXMLReader.m */, ); path = XML; sourceTree = ""; @@ -1183,6 +1599,8 @@ 25160E8D145652E40060A5C5 /* Vendor */ = { isa = PBXGroup; children = ( + 49A66B0614CEFAD800A6F062 /* XMLReader */, + 49D2759714C9E83E0090845D /* iso8601parser */, 25160E8E1456532C0060A5C5 /* cocoa-oauth */, 25160E941456532C0060A5C5 /* FileMD5Hash */, 25160E9B1456532C0060A5C5 /* JSONKit */, @@ -1269,32 +1687,41 @@ path = SOCKit; sourceTree = ""; }; - 25160FC51456F2330060A5C5 /* Specs */ = { + 25160FC51456F2330060A5C5 /* Tests */ = { isa = PBXGroup; children = ( + 251610351456F2330060A5C5 /* RKTestEnvironment.h */, + 251610361456F2330060A5C5 /* RKTestEnvironment.m */, 25160FC61456F2330060A5C5 /* CoreData */, 25160FCD1456F2330060A5C5 /* Fixtures */, 25160FF31456F2330060A5C5 /* Models */, 2516100B1456F2330060A5C5 /* Network */, 2516101B1456F2330060A5C5 /* ObjectMapping */, - 251610281456F2330060A5C5 /* Runner */, - 251610471456F2330060A5C5 /* Server */, 251610511456F2330060A5C5 /* Support */, + 251610281456F2330060A5C5 /* Vendor */, + 251610471456F2330060A5C5 /* Server */, ); - path = Specs; + path = Tests; sourceTree = ""; }; 25160FC61456F2330060A5C5 /* CoreData */ = { isa = PBXGroup; children = ( - 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderSpec.m */, - 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m */, - 25160FC91456F2330060A5C5 /* RKManagedObjectMappingSpec.m */, - 25160FCA1456F2330060A5C5 /* RKManagedObjectSpec.m */, - 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreSpec.m */, - 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m */, - ); - path = CoreData; + 252EFB0614D98F4D004863C8 /* RKSearchableManagedObjectTest.m */, + 252EFB0714D98F4D004863C8 /* RKSearchWordObserverTest.m */, + 25160FC71456F2330060A5C5 /* RKManagedObjectLoaderTest.m */, + 25160FC81456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m */, + 25160FC91456F2330060A5C5 /* RKManagedObjectMappingTest.m */, + 25160FCB1456F2330060A5C5 /* RKManagedObjectStoreTest.m */, + 25160FCC1456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m */, + 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */, + 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */, + 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */, + 259D98591550C6BE008C90F5 /* RKEntityByAttributeCacheTest.m */, + 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */, + ); + name = CoreData; + path = Logic/CoreData; sourceTree = ""; }; 25160FCD1456F2330060A5C5 /* Fixtures */ = { @@ -1319,6 +1746,8 @@ 25160FD01456F2330060A5C5 /* JSON */ = { isa = PBXGroup; children = ( + 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */, + 252EFB2714DA0689004863C8 /* NakedEvents.json */, 25160FD11456F2330060A5C5 /* ArrayOfNestedDictionaries.json */, 25160FD21456F2330060A5C5 /* ArrayOfResults.json */, 25160FD31456F2330060A5C5 /* ComplexNestedUser.json */, @@ -1336,6 +1765,8 @@ 25160FE61456F2330060A5C5 /* SameKeyDifferentTargetClasses.json */, 25160FE71456F2330060A5C5 /* user.json */, 25160FE81456F2330060A5C5 /* users.json */, + 25CAAA9315254E7800CAE5D7 /* ArrayOfHumans.json */, + 25119FB5154A34B400C6BC58 /* parents_and_children.json */, ); path = JSON; sourceTree = ""; @@ -1344,9 +1775,11 @@ isa = PBXGroup; children = ( 25160FD61456F2330060A5C5 /* boy.json */, + 73D3907314CA1A4A0093E3D6 /* child.json */, 25160FD71456F2330060A5C5 /* friends.json */, 25160FD81456F2330060A5C5 /* girl.json */, 25160FD91456F2330060A5C5 /* mixed.json */, + 73D3907114CA19F90093E3D6 /* parent.json */, ); path = Dynamic; sourceTree = ""; @@ -1373,6 +1806,7 @@ isa = PBXGroup; children = ( 25160FED1456F2330060A5C5 /* attributes_without_text_content.xml */, + 73D3907814CA1D710093E3D6 /* channels.xml */, 25160FEE1456F2330060A5C5 /* container_attributes.xml */, 25160FEF1456F2330060A5C5 /* national_weather_service.xml */, 25160FF01456F2330060A5C5 /* orders.xml */, @@ -1385,6 +1819,14 @@ 25160FF31456F2330060A5C5 /* Models */ = { isa = PBXGroup; children = ( + 252EFAF814D8EAEC004863C8 /* RKEvent.h */, + 252EFAF914D8EAEC004863C8 /* RKEvent.m */, + 25B6E9D514CF912500B1E881 /* RKSearchable.h */, + 25B6E9D614CF912500B1E881 /* RKSearchable.m */, + 25B6E9D714CF912500B1E881 /* RKTestAddress.h */, + 25B6E9D814CF912500B1E881 /* RKTestAddress.m */, + 25B6E9D914CF912500B1E881 /* RKTestUser.h */, + 25B6E9DA14CF912500B1E881 /* RKTestUser.m */, 25160FF41456F2330060A5C5 /* Data Model.xcdatamodel */, 25160FF51456F2330060A5C5 /* RKCat.h */, 25160FF61456F2330060A5C5 /* RKCat.m */, @@ -1400,10 +1842,10 @@ 251610001456F2330060A5C5 /* RKMappableAssociation.m */, 251610011456F2330060A5C5 /* RKMappableObject.h */, 251610021456F2330060A5C5 /* RKMappableObject.m */, - 251610031456F2330060A5C5 /* RKObjectLoaderSpecResultModel.h */, - 251610041456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m */, - 251610051456F2330060A5C5 /* RKObjectMapperSpecModel.h */, - 251610061456F2330060A5C5 /* RKObjectMapperSpecModel.m */, + 251610031456F2330060A5C5 /* RKObjectLoaderTestResultModel.h */, + 251610041456F2330060A5C5 /* RKObjectLoaderTestResultModel.m */, + 251610051456F2330060A5C5 /* RKObjectMapperTestModel.h */, + 251610061456F2330060A5C5 /* RKObjectMapperTestModel.m */, 251610071456F2330060A5C5 /* RKParent.h */, 251610081456F2330060A5C5 /* RKParent.m */, 251610091456F2330060A5C5 /* RKResident.h */, @@ -1415,55 +1857,52 @@ 2516100B1456F2330060A5C5 /* Network */ = { isa = PBXGroup; children = ( - 251610101456F2330060A5C5 /* RKAuthenticationSpec.m */, - 251610111456F2330060A5C5 /* RKClientSpec.m */, - 251610121456F2330060A5C5 /* RKOAuthClientSpec.m */, - 251610131456F2330060A5C5 /* RKParamsAttachmentSpec.m */, - 251610141456F2330060A5C5 /* RKParamsSpec.m */, - 251610171456F2330060A5C5 /* RKRequestQueueSpec.m */, - 251610181456F2330060A5C5 /* RKRequestSpec.m */, - 251610191456F2330060A5C5 /* RKResponseSpec.m */, - 2516101A1456F2330060A5C5 /* RKURLSpec.m */, - ); - path = Network; + 251610101456F2330060A5C5 /* RKAuthenticationTest.m */, + 251610111456F2330060A5C5 /* RKClientTest.m */, + 251610121456F2330060A5C5 /* RKOAuthClientTest.m */, + 251610131456F2330060A5C5 /* RKParamsAttachmentTest.m */, + 251610141456F2330060A5C5 /* RKParamsTest.m */, + 251610171456F2330060A5C5 /* RKRequestQueueTest.m */, + 251610181456F2330060A5C5 /* RKRequestTest.m */, + 251610191456F2330060A5C5 /* RKResponseTest.m */, + 2516101A1456F2330060A5C5 /* RKURLTest.m */, + ); + name = Network; + path = Logic/Network; sourceTree = ""; }; 2516101B1456F2330060A5C5 /* ObjectMapping */ = { isa = PBXGroup; children = ( - 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingSpec.m */, - 2516101D1456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm */, - 2516101E1456F2330060A5C5 /* RKObjectLoaderSpec.m */, - 2516101F1456F2330060A5C5 /* RKObjectManagerSpec.m */, - 251610211456F2330060A5C5 /* RKObjectMappingNextGenSpec.m */, - 251610221456F2330060A5C5 /* RKObjectMappingOperationSpec.m */, - 251610231456F2330060A5C5 /* RKObjectMappingProviderSpec.m */, - 251610241456F2330060A5C5 /* RKObjectMappingResultSpec.m */, - 251610251456F2330060A5C5 /* RKObjectRouterSpec.m */, - 251610261456F2330060A5C5 /* RKObjectSerializerSpec.m */, - 251610271456F2330060A5C5 /* RKParserRegistrySpec.m */, - ); - path = ObjectMapping; + 2516101C1456F2330060A5C5 /* RKDynamicObjectMappingTest.m */, + 2516101E1456F2330060A5C5 /* RKObjectLoaderTest.m */, + 2516101F1456F2330060A5C5 /* RKObjectManagerTest.m */, + 251610211456F2330060A5C5 /* RKObjectMappingNextGenTest.m */, + 251610221456F2330060A5C5 /* RKObjectMappingOperationTest.m */, + 251610231456F2330060A5C5 /* RKObjectMappingProviderTest.m */, + 251610241456F2330060A5C5 /* RKObjectMappingResultTest.m */, + 251610251456F2330060A5C5 /* RKObjectRouterTest.m */, + 251610261456F2330060A5C5 /* RKObjectSerializerTest.m */, + 251610271456F2330060A5C5 /* RKParserRegistryTest.m */, + 254A62BF14AD591C00939BEE /* RKObjectPaginatorTest.m */, + ); + name = ObjectMapping; + path = Logic/ObjectMapping; sourceTree = ""; }; - 251610281456F2330060A5C5 /* Runner */ = { + 251610281456F2330060A5C5 /* Vendor */ = { isa = PBXGroup; children = ( 250CA67A147D8E7F0047D347 /* OCHamcrest */, 2516102B1456F2330060A5C5 /* OCMock */, - 251610351456F2330060A5C5 /* RKSpecEnvironment.h */, - 251610361456F2330060A5C5 /* RKSpecEnvironment.m */, - 251610371456F2330060A5C5 /* RKSpecResponseLoader.h */, - 251610381456F2330060A5C5 /* RKSpecResponseLoader.m */, - 251610391456F2330060A5C5 /* set_ip_address.scpt */, ); - path = Runner; + path = Vendor; sourceTree = ""; }; 2516102B1456F2330060A5C5 /* OCMock */ = { isa = PBXGroup; children = ( - 25A34190147C2F370009758D /* OCMock */, + 25B6E97A14CF829400B1E881 /* OCMock */, ); path = OCMock; sourceTree = ""; @@ -1497,6 +1936,7 @@ 2516104A1456F2330060A5C5 /* network */ = { isa = PBXGroup; children = ( + 41A4EBF715374D1800740BC8 /* redirection.rb */, 2516104B1456F2330060A5C5 /* authentication.rb */, 2516104C1456F2330060A5C5 /* etags.rb */, 2516104D1456F2330060A5C5 /* oauth2.rb */, @@ -1508,69 +1948,190 @@ 251610511456F2330060A5C5 /* Support */ = { isa = PBXGroup; children = ( - 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationSpec.m */, - 251610531456F2330060A5C5 /* NSStringRestKitSpec.m */, - 251610541456F2330060A5C5 /* RKDotNetDateFormatterSpec.m */, - 251610551456F2330060A5C5 /* RKJSONParserJSONKitSpec.m */, - 251610561456F2330060A5C5 /* RKPathMatcherSpec.m */, - 251610571456F2330060A5C5 /* RKXMLParserSpec.m */, - ); - path = Support; + 252EFB0C14D98F76004863C8 /* RKMutableBlockDictionaryTest.m */, + 251610521456F2330060A5C5 /* NSDictionary+RKRequestSerializationTest.m */, + 251610531456F2330060A5C5 /* NSStringRestKitTest.m */, + 251610541456F2330060A5C5 /* RKDotNetDateFormatterTest.m */, + 251610551456F2330060A5C5 /* RKJSONParserJSONKitTest.m */, + 251610561456F2330060A5C5 /* RKPathMatcherTest.m */, + 251610571456F2330060A5C5 /* RKXMLParserTest.m */, + 252A2033153477870078F8AD /* NSArray+RKAdditionsTest.m */, + 2501405215366000004E0466 /* RKObjectiveCppTest.mm */, + 25A2476D153E667E003240B6 /* RKCacheTest.m */, + ); + name = Support; + path = Logic/Support; + sourceTree = ""; + }; + 252EFB1F14D9A8D4004863C8 /* Testing */ = { + isa = PBXGroup; + children = ( + 25055B8214EEF32A00B9C4DD /* RKTestFactory.h */, + 25055B8314EEF32A00B9C4DD /* RKTestFactory.m */, + 25FABED414E37A2B00E609E7 /* RKTestResponseLoader.h */, + 25FABED514E37A2B00E609E7 /* RKTestResponseLoader.m */, + 252EFB2014D9B35D004863C8 /* RKTestFixture.h */, + 252EFB2114D9B35D004863C8 /* RKTestFixture.m */, + 252EFAFC14D8EB30004863C8 /* RKTestNotificationObserver.h */, + 252EFAFD14D8EB30004863C8 /* RKTestNotificationObserver.m */, + 25055B8014EEF32A00B9C4DD /* RKMappingTest.h */, + 25055B8114EEF32A00B9C4DD /* RKMappingTest.m */, + 25055B8D14EEF40000B9C4DD /* RKMappingTestExpectation.h */, + 25055B8E14EEF40000B9C4DD /* RKMappingTestExpectation.m */, + 252EFB2414D9B6F2004863C8 /* Testing.h */, + 25C954A415542A47005C9E08 /* RKTestConstants.m */, + 25E4DAB2156DA97F00A5C84B /* RKTableControllerTestDelegate.h */, + 25E4DAB3156DA97F00A5C84B /* RKTableControllerTestDelegate.m */, + ); + name = Testing; sourceTree = ""; }; - 25A34190147C2F370009758D /* OCMock */ = { + 25B6E90214CF778D00B1E881 /* UI */ = { isa = PBXGroup; children = ( - 25A34191147C2F370009758D /* InfoPlist.strings */, - 25A34193147C2F370009758D /* NSInvocation+OCMAdditions.h */, - 25A34194147C2F370009758D /* NSInvocation+OCMAdditions.m */, - 25A34195147C2F370009758D /* NSMethodSignature+OCMAdditions.h */, - 25A34196147C2F370009758D /* NSMethodSignature+OCMAdditions.m */, - 25A34197147C2F370009758D /* NSNotificationCenter+OCMAdditions.h */, - 25A34198147C2F370009758D /* NSNotificationCenter+OCMAdditions.m */, - 25A34199147C2F370009758D /* OCClassMockObject.h */, - 25A3419A147C2F370009758D /* OCClassMockObject.m */, - 25A3419B147C2F370009758D /* OCMArg.h */, - 25A3419C147C2F370009758D /* OCMArg.m */, - 25A3419D147C2F370009758D /* OCMBlockCaller.h */, - 25A3419E147C2F370009758D /* OCMBlockCaller.m */, - 25A3419F147C2F370009758D /* OCMBoxedReturnValueProvider.h */, - 25A341A0147C2F370009758D /* OCMBoxedReturnValueProvider.m */, - 25A341A1147C2F370009758D /* OCMConstraint.h */, - 25A341A2147C2F370009758D /* OCMConstraint.m */, - 25A341A3147C2F370009758D /* OCMExceptionReturnValueProvider.h */, - 25A341A4147C2F370009758D /* OCMExceptionReturnValueProvider.m */, - 25A341A5147C2F370009758D /* OCMIndirectReturnValueProvider.h */, - 25A341A6147C2F370009758D /* OCMIndirectReturnValueProvider.m */, - 25A341A7147C2F370009758D /* OCMNotificationPoster.h */, - 25A341A8147C2F370009758D /* OCMNotificationPoster.m */, - 25A341A9147C2F370009758D /* OCMObserverRecorder.h */, - 25A341AA147C2F370009758D /* OCMObserverRecorder.m */, - 25A341AB147C2F370009758D /* OCMock-Info.plist */, - 25A341AC147C2F370009758D /* OCMock-Prefix.pch */, - 25A341AD147C2F370009758D /* OCMock.h */, - 25A341AE147C2F370009758D /* OCMockObject.h */, - 25A341AF147C2F370009758D /* OCMockObject.m */, - 25A341B0147C2F370009758D /* OCMockRecorder.h */, - 25A341B1147C2F370009758D /* OCMockRecorder.m */, - 25A341B2147C2F370009758D /* OCMPassByRefSetter.h */, - 25A341B3147C2F370009758D /* OCMPassByRefSetter.m */, - 25A341B4147C2F370009758D /* OCMRealObjectForwarder.h */, - 25A341B5147C2F370009758D /* OCMRealObjectForwarder.m */, - 25A341B6147C2F370009758D /* OCMReturnValueProvider.h */, - 25A341B7147C2F370009758D /* OCMReturnValueProvider.m */, - 25A341B8147C2F370009758D /* OCObserverMockObject.h */, - 25A341B9147C2F370009758D /* OCObserverMockObject.m */, - 25A341BA147C2F370009758D /* OCPartialMockObject.h */, - 25A341BB147C2F370009758D /* OCPartialMockObject.m */, - 25A341BC147C2F370009758D /* OCPartialMockRecorder.h */, - 25A341BD147C2F370009758D /* OCPartialMockRecorder.m */, - 25A341BE147C2F370009758D /* OCProtocolMockObject.h */, - 25A341BF147C2F370009758D /* OCProtocolMockObject.m */, + 25EC1AB814F8019F00C3CF3F /* RKRefreshGestureRecognizer.h */, + 25EC1AB914F8019F00C3CF3F /* RKRefreshGestureRecognizer.m */, + 25EC1ABA14F8019F00C3CF3F /* RKRefreshTriggerView.h */, + 25EC1ABB14F8019F00C3CF3F /* RKRefreshTriggerView.m */, + 25B6E90314CF778D00B1E881 /* RKAbstractTableController.h */, + 25B6E90414CF778D00B1E881 /* RKAbstractTableController.m */, + 25B6E90614CF778D00B1E881 /* RKAbstractTableController_Internals.h */, + 25B6E90714CF778D00B1E881 /* RKControlTableItem.h */, + 25B6E90814CF778D00B1E881 /* RKControlTableItem.m */, + 25B6E90914CF778D00B1E881 /* RKControlTableViewCell.h */, + 25B6E90A14CF778D00B1E881 /* RKControlTableViewCell.m */, + 25B6E90B14CF778D00B1E881 /* RKFetchedResultsTableController.h */, + 25B6E90C14CF778D00B1E881 /* RKFetchedResultsTableController.m */, + 25B6E90D14CF778D00B1E881 /* RKForm.h */, + 25B6E90E14CF778D00B1E881 /* RKForm.m */, + 25B6E90F14CF778D00B1E881 /* RKFormSection.h */, + 25B6E91014CF778D00B1E881 /* RKFormSection.m */, + 25B6E91114CF778D00B1E881 /* RKTableController.h */, + 25B6E91214CF778D00B1E881 /* RKTableController.m */, + 25B6E91314CF778D00B1E881 /* RKTableItem.h */, + 25B6E91414CF778D00B1E881 /* RKTableItem.m */, + 25B6E91514CF778D00B1E881 /* RKTableSection.h */, + 25B6E91614CF778D00B1E881 /* RKTableSection.m */, + 25B6E91714CF778D00B1E881 /* RKTableViewCellMapping.h */, + 25B6E91814CF778D00B1E881 /* RKTableViewCellMapping.m */, + 25B6E91914CF778D00B1E881 /* RKTableViewCellMappings.h */, + 25B6E91A14CF778D00B1E881 /* RKTableViewCellMappings.m */, + 25B6E91B14CF778D00B1E881 /* UI.h */, + 25B6E91C14CF778D00B1E881 /* UIView+FindFirstResponder.h */, + 25B6E91D14CF778D00B1E881 /* UIView+FindFirstResponder.m */, + 25EC1A2A14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.h */, + 25EC1A2B14F6FDAC00C3CF3F /* RKObjectManager+RKTableController.m */, + 25EC1B3714F84B5C00C3CF3F /* UIImage+RKAdditions.h */, + 25EC1B3814F84B5C00C3CF3F /* UIImage+RKAdditions.m */, + ); + path = UI; + sourceTree = ""; + }; + 25B6E97A14CF829400B1E881 /* OCMock */ = { + isa = PBXGroup; + children = ( + 25B6E97B14CF829400B1E881 /* InfoPlist.strings */, + 25B6E97D14CF829400B1E881 /* NSInvocation+OCMAdditions.h */, + 25B6E97E14CF829400B1E881 /* NSInvocation+OCMAdditions.m */, + 25B6E97F14CF829400B1E881 /* NSMethodSignature+OCMAdditions.h */, + 25B6E98014CF829400B1E881 /* NSMethodSignature+OCMAdditions.m */, + 25B6E98114CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m */, + 25B6E98214CF829400B1E881 /* OCClassMockObject.h */, + 25B6E98314CF829400B1E881 /* OCClassMockObject.m */, + 25B6E98414CF829400B1E881 /* OCMArg.m */, + 25B6E98514CF829400B1E881 /* OCMBlockCaller.h */, + 25B6E98614CF829400B1E881 /* OCMBlockCaller.m */, + 25B6E98714CF829400B1E881 /* OCMBoxedReturnValueProvider.h */, + 25B6E98814CF829400B1E881 /* OCMBoxedReturnValueProvider.m */, + 25B6E98914CF829400B1E881 /* OCMConstraint.m */, + 25B6E98A14CF829400B1E881 /* OCMExceptionReturnValueProvider.h */, + 25B6E98B14CF829400B1E881 /* OCMExceptionReturnValueProvider.m */, + 25B6E98C14CF829400B1E881 /* OCMIndirectReturnValueProvider.h */, + 25B6E98D14CF829400B1E881 /* OCMIndirectReturnValueProvider.m */, + 25B6E98E14CF829400B1E881 /* OCMNotificationPoster.h */, + 25B6E98F14CF829400B1E881 /* OCMNotificationPoster.m */, + 25B6E99014CF829400B1E881 /* OCMObserverRecorder.h */, + 25B6E99114CF829400B1E881 /* OCMObserverRecorder.m */, + 25B6E99214CF829400B1E881 /* OCMock-Info.plist */, + 25B6E99314CF829400B1E881 /* OCMock-Prefix.pch */, + 25B6E99414CF829400B1E881 /* OCMockObject.m */, + 25B6E99514CF829400B1E881 /* OCMockRecorder.m */, + 25B6E99614CF829400B1E881 /* OCMPassByRefSetter.h */, + 25B6E99714CF829400B1E881 /* OCMPassByRefSetter.m */, + 25B6E99814CF829400B1E881 /* OCMRealObjectForwarder.h */, + 25B6E99914CF829400B1E881 /* OCMRealObjectForwarder.m */, + 25B6E99A14CF829400B1E881 /* OCMReturnValueProvider.h */, + 25B6E99B14CF829400B1E881 /* OCMReturnValueProvider.m */, + 25B6E99C14CF829400B1E881 /* OCObserverMockObject.h */, + 25B6E99D14CF829400B1E881 /* OCObserverMockObject.m */, + 25B6E99E14CF829400B1E881 /* OCPartialMockObject.h */, + 25B6E99F14CF829400B1E881 /* OCPartialMockObject.m */, + 25B6E9A014CF829400B1E881 /* OCPartialMockRecorder.h */, + 25B6E9A114CF829400B1E881 /* OCPartialMockRecorder.m */, + 25B6E9A214CF829400B1E881 /* OCProtocolMockObject.h */, + 25B6E9A314CF829400B1E881 /* OCProtocolMockObject.m */, ); path = OCMock; sourceTree = ""; }; + 25EC1AD614F8022600C3CF3F /* Resources */ = { + isa = PBXGroup; + children = ( + 25EC1ADB14F8022600C3CF3F /* RestKitCoreData.xcdatamodeld */, + 25EC1B1E14F821B500C3CF3F /* RestKitResources-Prefix.pch */, + 25EC1AD714F8022600C3CF3F /* PLISTs */, + 25EC1ADD14F8022600C3CF3F /* RKRefreshTriggerViewAssets */, + ); + path = Resources; + sourceTree = ""; + }; + 25EC1AD714F8022600C3CF3F /* PLISTs */ = { + isa = PBXGroup; + children = ( + 25EC1B1F14F8220800C3CF3F /* RestKitResources-Info.plist */, + 25EC1AD814F8022600C3CF3F /* RestKitFramework-Info.plist */, + 25EC1AD914F8022600C3CF3F /* RestKitFrameworkTests-Info.plist */, + 25EC1ADA14F8022600C3CF3F /* RestKitTests-Info.plist */, + ); + path = PLISTs; + sourceTree = ""; + }; + 25EC1ADD14F8022600C3CF3F /* RKRefreshTriggerViewAssets */ = { + isa = PBXGroup; + children = ( + 25EC1ADE14F8022600C3CF3F /* blackArrow.png */, + 25EC1ADF14F8022600C3CF3F /* blackArrow@2x.png */, + 25EC1AE014F8022600C3CF3F /* blueArrow.png */, + 25EC1AE114F8022600C3CF3F /* blueArrow@2x.png */, + 25EC1AE214F8022600C3CF3F /* grayArrow.png */, + 25EC1AE314F8022600C3CF3F /* grayArrow@2x.png */, + 25EC1AE414F8022600C3CF3F /* whiteArrow.png */, + 25EC1AE514F8022600C3CF3F /* whiteArrow@2x.png */, + ); + path = RKRefreshTriggerViewAssets; + sourceTree = ""; + }; + 49A66B0614CEFAD800A6F062 /* XMLReader */ = { + isa = PBXGroup; + children = ( + 49A66B0814CEFB0400A6F062 /* LICENCE */, + 49A66B0914CEFB0400A6F062 /* XMLReader.h */, + 49A66B0A14CEFB0400A6F062 /* XMLReader.m */, + ); + name = XMLReader; + sourceTree = ""; + }; + 49D2759714C9E83E0090845D /* iso8601parser */ = { + isa = PBXGroup; + children = ( + 49D2759914C9EF1E0090845D /* ISO8601DateFormatter.h */, + 49D2759A14C9EF1E0090845D /* ISO8601DateFormatter.m */, + 49D2759B14C9EF1E0090845D /* LICENSE.txt */, + 49D2759C14C9EF1E0090845D /* README.txt */, + ); + name = iso8601parser; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1579,8 +2140,8 @@ buildActionMask = 2147483647; files = ( 25160DD5145650490060A5C5 /* CoreData.h in Headers */, + 25FABED614E37A2B00E609E7 /* RKTestResponseLoader.h in Headers */, 25160DD6145650490060A5C5 /* NSManagedObject+ActiveRecord.h in Headers */, - 25160DD8145650490060A5C5 /* RKManagedObjectCache.h in Headers */, 25160DD9145650490060A5C5 /* RKManagedObjectLoader.h in Headers */, 25160DDB145650490060A5C5 /* RKManagedObjectMapping.h in Headers */, 25160DDD145650490060A5C5 /* RKManagedObjectMappingOperation.h in Headers */, @@ -1589,9 +2150,8 @@ 25160DE3145650490060A5C5 /* RKManagedObjectThreadSafeInvocation.h in Headers */, 25160DE5145650490060A5C5 /* RKObjectPropertyInspector+CoreData.h in Headers */, 25160DE7145650490060A5C5 /* Network.h in Headers */, - 25160DE8145650490060A5C5 /* NSData+MD5.h in Headers */, + 25160DE8145650490060A5C5 /* NSData+RKAdditions.h in Headers */, 25160DEA145650490060A5C5 /* NSDictionary+RKRequestSerialization.h in Headers */, - 25160DEC145650490060A5C5 /* NSString+MD5.h in Headers */, 25160DEE145650490060A5C5 /* RKClient.h in Headers */, 25160DF0145650490060A5C5 /* RKNotifications.h in Headers */, 25160DF2145650490060A5C5 /* RKOAuthClient.h in Headers */, @@ -1628,15 +2188,13 @@ 25160E29145650490060A5C5 /* RKObjectSerializer.h in Headers */, 25160E2B145650490060A5C5 /* RKParserRegistry.h in Headers */, 25160E2D145650490060A5C5 /* RKRouter.h in Headers */, - 25160E2F145650490060A5C5 /* Errors.h in Headers */, 25160E31145650490060A5C5 /* lcl_config_components.h in Headers */, 25160E32145650490060A5C5 /* lcl_config_extensions.h in Headers */, 25160E33145650490060A5C5 /* lcl_config_logger.h in Headers */, 25160E34145650490060A5C5 /* NSDictionary+RKAdditions.h in Headers */, - 25160E36145650490060A5C5 /* NSString+RestKit.h in Headers */, - 25160E38145650490060A5C5 /* NSURL+RestKit.h in Headers */, + 25160E36145650490060A5C5 /* NSString+RKAdditions.h in Headers */, + 25160E38145650490060A5C5 /* NSURL+RKAdditions.h in Headers */, 25160E3A145650490060A5C5 /* RKJSONParserJSONKit.h in Headers */, - 25160E42145650490060A5C5 /* RKXMLParserLibXML.h in Headers */, 25160E44145650490060A5C5 /* RestKit-Prefix.pch in Headers */, 25160E45145650490060A5C5 /* RKAlert.h in Headers */, 25160E47145650490060A5C5 /* RKDotNetDateFormatter.h in Headers */, @@ -1659,7 +2217,62 @@ 25160EF81456532C0060A5C5 /* LCLNSLog.h in Headers */, 25160F081456532C0060A5C5 /* SOCKit.h in Headers */, 25160E2E145650490060A5C5 /* RestKit.h in Headers */, + 254A62B914AD544200939BEE /* RKObjectPaginator.h in Headers */, 25B408261491CDDC00F21111 /* RKDirectory.h in Headers */, + 2513504E14B8FE6B00A7E893 /* RKConfigurationDelegate.h in Headers */, + 250DF22A14C5190E0001DEFA /* RKOrderedDictionary.h in Headers */, + 49D2759D14C9EF1E0090845D /* ISO8601DateFormatter.h in Headers */, + 49A66B0C14CEFB0400A6F062 /* XMLReader.h in Headers */, + 250DF25F14C680F90001DEFA /* RKObjectMappingProvider+Contexts.h in Headers */, + 49D275AD14C9F3020090845D /* RKISO8601DateFormatter.h in Headers */, + 25B6E91E14CF778D00B1E881 /* RKAbstractTableController.h in Headers */, + 25B6E92314CF778D00B1E881 /* RKAbstractTableController_Internals.h in Headers */, + 25B6E92514CF778D00B1E881 /* RKControlTableItem.h in Headers */, + 25B6E92914CF778D00B1E881 /* RKControlTableViewCell.h in Headers */, + 25B6E92D14CF778D00B1E881 /* RKFetchedResultsTableController.h in Headers */, + 25B6E93114CF778D00B1E881 /* RKForm.h in Headers */, + 25B6E93514CF778D00B1E881 /* RKFormSection.h in Headers */, + 25B6E93914CF778D00B1E881 /* RKTableController.h in Headers */, + 25B6E93D14CF778D00B1E881 /* RKTableItem.h in Headers */, + 25B6E94114CF778D00B1E881 /* RKTableSection.h in Headers */, + 25B6E94514CF778D00B1E881 /* RKTableViewCellMapping.h in Headers */, + 25B6E94914CF778D00B1E881 /* RKTableViewCellMappings.h in Headers */, + 25B6E94D14CF778D00B1E881 /* UI.h in Headers */, + 25B6E94F14CF778D00B1E881 /* UIView+FindFirstResponder.h in Headers */, + 25B6E95514CF795D00B1E881 /* RKErrors.h in Headers */, + 25B6E95C14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h in Headers */, + 25B6E9E914CF940700B1E881 /* RKManagedObjectSearchEngine.h in Headers */, + 25B6E9ED14CF940700B1E881 /* RKSearchableManagedObject.h in Headers */, + 25B6E9F114CF940700B1E881 /* RKSearchWord.h in Headers */, + 25B6E9F514CF940700B1E881 /* RKSearchWordObserver.h in Headers */, + 25B6E9FD14CF943E00B1E881 /* RKCache.h in Headers */, + 25B6EA0114CF943E00B1E881 /* RKMutableBlockDictionary.h in Headers */, + 49A66B1214CF03CA00A6F062 /* RKXMLParserXMLReader.h in Headers */, + 252EFB1B14D9A7CB004863C8 /* NSBundle+RKAdditions.h in Headers */, + 252EFB2514D9B6F2004863C8 /* Testing.h in Headers */, + 253B495214E35D1A00B0483F /* RKTestFixture.h in Headers */, + 25FABED214E3796B00E609E7 /* RKTestNotificationObserver.h in Headers */, + 25055B8414EEF32A00B9C4DD /* RKMappingTest.h in Headers */, + 25055B8814EEF32A00B9C4DD /* RKTestFactory.h in Headers */, + 25055B8F14EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */, + 25EC1A2C14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.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 /* 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; }; @@ -1718,7 +2331,6 @@ 25160F68145655C60060A5C5 /* RKRouter.h in Headers */, 25160F69145655D10060A5C5 /* CoreData.h in Headers */, 25160F6A145655D10060A5C5 /* NSManagedObject+ActiveRecord.h in Headers */, - 25160F6C145655D10060A5C5 /* RKManagedObjectCache.h in Headers */, 25160F6D145655D10060A5C5 /* RKManagedObjectLoader.h in Headers */, 25160F6F145655D10060A5C5 /* RKManagedObjectMapping.h in Headers */, 25160F71145655D10060A5C5 /* RKManagedObjectMappingOperation.h in Headers */, @@ -1726,15 +2338,13 @@ 25160F75145655D10060A5C5 /* RKManagedObjectStore.h in Headers */, 25160F77145655D10060A5C5 /* RKManagedObjectThreadSafeInvocation.h in Headers */, 25160F79145655D10060A5C5 /* RKObjectPropertyInspector+CoreData.h in Headers */, - 25160F7F145657650060A5C5 /* NSData+MD5.h in Headers */, - 25160F81145657650060A5C5 /* NSString+MD5.h in Headers */, - 25160F83145657650060A5C5 /* Errors.h in Headers */, + 25160F7F145657650060A5C5 /* NSData+RKAdditions.h in Headers */, 25160F85145657650060A5C5 /* lcl_config_components.h in Headers */, 25160F86145657650060A5C5 /* lcl_config_extensions.h in Headers */, 25160F87145657650060A5C5 /* lcl_config_logger.h in Headers */, 25160F88145657650060A5C5 /* NSDictionary+RKAdditions.h in Headers */, - 25160F8A145657650060A5C5 /* NSString+RestKit.h in Headers */, - 25160F8C145657650060A5C5 /* NSURL+RestKit.h in Headers */, + 25160F8A145657650060A5C5 /* NSString+RKAdditions.h in Headers */, + 25160F8C145657650060A5C5 /* NSURL+RKAdditions.h in Headers */, 25160F8E1456576C0060A5C5 /* RKAlert.h in Headers */, 25160F901456576C0060A5C5 /* RKDotNetDateFormatter.h in Headers */, 25160F921456576C0060A5C5 /* RKFixCategoryBug.h in Headers */, @@ -1747,6 +2357,47 @@ 25160F9E1456577F0060A5C5 /* RKJSONParserJSONKit.h in Headers */, 25160F25145655AF0060A5C5 /* RestKit.h in Headers */, 25B408271491CDDC00F21111 /* RKDirectory.h in Headers */, + 2513504F14B8FE6B00A7E893 /* RKConfigurationDelegate.h in Headers */, + 254A62BA14AD544200939BEE /* RKObjectPaginator.h in Headers */, + 250DF22B14C5190E0001DEFA /* RKOrderedDictionary.h in Headers */, + 49A66B1314CF03CA00A6F062 /* RKXMLParserXMLReader.h in Headers */, + 49D2759E14C9EF1E0090845D /* ISO8601DateFormatter.h in Headers */, + 250DF26014C680F90001DEFA /* RKObjectMappingProvider+Contexts.h in Headers */, + 49D275AE14C9F3020090845D /* RKISO8601DateFormatter.h in Headers */, + 25B6E95614CF795D00B1E881 /* RKErrors.h in Headers */, + 25B6E95D14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.h in Headers */, + 25B6E9EA14CF940700B1E881 /* RKManagedObjectSearchEngine.h in Headers */, + 25B6E9EE14CF940700B1E881 /* RKSearchableManagedObject.h in Headers */, + 25B6E9F214CF940700B1E881 /* RKSearchWord.h in Headers */, + 25B6E9F614CF940700B1E881 /* RKSearchWordObserver.h in Headers */, + 25B6E9FE14CF943E00B1E881 /* RKCache.h in Headers */, + 25B6EA0214CF943E00B1E881 /* RKMutableBlockDictionary.h in Headers */, + 252EFB1C14D9A7CB004863C8 /* NSBundle+RKAdditions.h in Headers */, + 252EFB2614D9B6F2004863C8 /* Testing.h in Headers */, + 25FABED714E37A2B00E609E7 /* RKTestResponseLoader.h in Headers */, + 49A66B0D14CEFB0400A6F062 /* XMLReader.h in Headers */, + 25FABED314E3796C00E609E7 /* RKTestNotificationObserver.h in Headers */, + 25055B8514EEF32A00B9C4DD /* RKMappingTest.h in Headers */, + 25055B8914EEF32A00B9C4DD /* RKTestFactory.h in Headers */, + 25055B9014EEF40000B9C4DD /* RKMappingTestExpectation.h in Headers */, + 25EC1A2D14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.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 /* 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; }; @@ -1760,8 +2411,6 @@ 25160D1214564E810060A5C5 /* Sources */, 25160D1314564E810060A5C5 /* Frameworks */, 25160D1414564E810060A5C5 /* Headers */, - 25BB65871485D15C00D62E64 /* Copy Headers to Legacy Build Location */, - 25BB65891485D17A00D62E64 /* Protect Copied Headers */, ); buildRules = ( ); @@ -1829,6 +2478,24 @@ productReference = 25160E78145651060060A5C5 /* RestKitFrameworkTests.octest */; productType = "com.apple.product-type.bundle"; }; + 259C301515128079003066A2 /* RestKitResources */ = { + isa = PBXNativeTarget; + buildConfigurationList = 259C301F15128079003066A2 /* Build configuration list for PBXNativeTarget "RestKitResources" */; + buildPhases = ( + 259C301215128079003066A2 /* Sources */, + 259C301315128079003066A2 /* Frameworks */, + 259C301415128079003066A2 /* Resources */, + 259C303B151289DD003066A2 /* Sync Resources to Project */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RestKitResources; + productName = RestKitResources; + productReference = 259C301615128079003066A2 /* RestKitResources.bundle */; + productType = "com.apple.product-type.bundle"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1854,6 +2521,7 @@ 25160D2514564E820060A5C5 /* RestKitTests */, 25160E61145651060060A5C5 /* RestKitFramework */, 25160E77145651060060A5C5 /* RestKitFrameworkTests */, + 259C301515128079003066A2 /* RestKitResources */, ); }; /* End PBXProject section */ @@ -1886,15 +2554,21 @@ 2516108C1456F2330060A5C5 /* SameKeyDifferentTargetClasses.json in Resources */, 2516108E1456F2330060A5C5 /* user.json in Resources */, 251610901456F2330060A5C5 /* users.json in Resources */, - 251610921456F2330060A5C5 /* .gitignore in Resources */, 251610961456F2330060A5C5 /* attributes_without_text_content.xml in Resources */, 251610981456F2330060A5C5 /* container_attributes.xml in Resources */, 2516109A1456F2330060A5C5 /* national_weather_service.xml in Resources */, 2516109C1456F2330060A5C5 /* orders.xml in Resources */, 2516109E1456F2330060A5C5 /* tab_data.xml in Resources */, 251610A01456F2330060A5C5 /* zend.xml in Resources */, - 25A341C0147C2F370009758D /* InfoPlist.strings in Resources */, - 25A341DA147C2F370009758D /* OCMock-Info.plist in Resources */, + 73D3907414CA1AE00093E3D6 /* parent.json in Resources */, + 73D3907614CA1AE60093E3D6 /* child.json in Resources */, + 73D3907914CA1DD40093E3D6 /* channels.xml in Resources */, + 25B6E9A414CF829400B1E881 /* InfoPlist.strings in Resources */, + 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; }; @@ -1942,15 +2616,36 @@ 2516109D1456F2330060A5C5 /* orders.xml in Resources */, 2516109F1456F2330060A5C5 /* tab_data.xml in Resources */, 251610A11456F2330060A5C5 /* zend.xml in Resources */, - 251610F51456F2340060A5C5 /* set_ip_address.scpt in Resources */, 251611031456F2340060A5C5 /* authentication.rb in Resources */, 251611051456F2340060A5C5 /* etags.rb in Resources */, 251611071456F2340060A5C5 /* oauth2.rb in Resources */, 251611091456F2340060A5C5 /* timeout.rb in Resources */, 2516110B1456F2340060A5C5 /* restkit.rb in Resources */, 2516110D1456F2340060A5C5 /* server.rb in Resources */, - 25A341C1147C2F370009758D /* InfoPlist.strings in Resources */, - 25A341DB147C2F370009758D /* OCMock-Info.plist in Resources */, + 73D3907514CA1AE20093E3D6 /* parent.json in Resources */, + 73D3907714CA1AE60093E3D6 /* child.json in Resources */, + 73D3907A14CA1DD50093E3D6 /* channels.xml in Resources */, + 25B6E9A514CF829400B1E881 /* InfoPlist.strings in Resources */, + 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; + }; + 259C301415128079003066A2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 259C3022151280A1003066A2 /* blackArrow.png in Resources */, + 259C3023151280A1003066A2 /* blackArrow@2x.png in Resources */, + 259C3024151280A1003066A2 /* blueArrow.png in Resources */, + 259C3025151280A1003066A2 /* blueArrow@2x.png in Resources */, + 259C3026151280A1003066A2 /* grayArrow.png in Resources */, + 259C3027151280A1003066A2 /* grayArrow@2x.png in Resources */, + 259C3028151280A1003066A2 /* whiteArrow.png in Resources */, + 259C3029151280A1003066A2 /* whiteArrow@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1968,7 +2663,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#\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n\n# Run the unit tests in this test bundle.\n\"${SRCROOT}/Tests/RunPlatformUnitTests\""; }; 25160E76145651060060A5C5 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1983,33 +2678,19 @@ shellPath = /bin/sh; shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; }; - 25BB65871485D15C00D62E64 /* Copy Headers to Legacy Build Location */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Copy Headers to Legacy Build Location"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = "/bin/sh Scripts/CopyHeadersToLegacyBuildDir.command"; - shellScript = ""; - }; - 25BB65891485D17A00D62E64 /* Protect Copied Headers */ = { + 259C303B151289DD003066A2 /* Sync Resources to Project */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Protect Copied Headers"; + name = "Sync Resources to Project"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = "/bin/sh Scripts/Protect.command"; - shellScript = ""; + shellPath = "/bin/bash -ex"; + shellScript = "rsync -av --delete ${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}/ ${SOURCE_ROOT}/${FULL_PRODUCT_NAME}"; }; /* End PBXShellScriptBuildPhase section */ @@ -2018,6 +2699,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 253B495F14E35EC300B0483F /* RKTestFixture.m in Sources */, 25160DD7145650490060A5C5 /* NSManagedObject+ActiveRecord.m in Sources */, 25160DDA145650490060A5C5 /* RKManagedObjectLoader.m in Sources */, 25160DDC145650490060A5C5 /* RKManagedObjectMapping.m in Sources */, @@ -2026,9 +2708,8 @@ 25160DE2145650490060A5C5 /* RKManagedObjectStore.m in Sources */, 25160DE4145650490060A5C5 /* RKManagedObjectThreadSafeInvocation.m in Sources */, 25160DE6145650490060A5C5 /* RKObjectPropertyInspector+CoreData.m in Sources */, - 25160DE9145650490060A5C5 /* NSData+MD5.m in Sources */, + 25160DE9145650490060A5C5 /* NSData+RKAdditions.m in Sources */, 25160DEB145650490060A5C5 /* NSDictionary+RKRequestSerialization.m in Sources */, - 25160DED145650490060A5C5 /* NSString+MD5.m in Sources */, 25160DEF145650490060A5C5 /* RKClient.m in Sources */, 25160DF1145650490060A5C5 /* RKNotifications.m in Sources */, 25160DF3145650490060A5C5 /* RKOAuthClient.m in Sources */, @@ -2057,12 +2738,10 @@ 25160E28145650490060A5C5 /* RKObjectRouter.m in Sources */, 25160E2A145650490060A5C5 /* RKObjectSerializer.m in Sources */, 25160E2C145650490060A5C5 /* RKParserRegistry.m in Sources */, - 25160E30145650490060A5C5 /* Errors.m in Sources */, 25160E35145650490060A5C5 /* NSDictionary+RKAdditions.m in Sources */, - 25160E37145650490060A5C5 /* NSString+RestKit.m in Sources */, - 25160E39145650490060A5C5 /* NSURL+RestKit.m in Sources */, + 25160E37145650490060A5C5 /* NSString+RKAdditions.m in Sources */, + 25160E39145650490060A5C5 /* NSURL+RKAdditions.m in Sources */, 25160E3B145650490060A5C5 /* RKJSONParserJSONKit.m in Sources */, - 25160E43145650490060A5C5 /* RKXMLParserLibXML.m in Sources */, 25160E46145650490060A5C5 /* RKAlert.m in Sources */, 25160E48145650490060A5C5 /* RKDotNetDateFormatter.m in Sources */, 25160E4B145650490060A5C5 /* RKLog.m in Sources */, @@ -2077,6 +2756,57 @@ 25160EFA1456532C0060A5C5 /* LCLNSLog.m in Sources */, 25160F0A1456532C0060A5C5 /* SOCKit.m in Sources */, 25B408281491CDDC00F21111 /* RKDirectory.m in Sources */, + 254A62BB14AD544200939BEE /* RKObjectPaginator.m in Sources */, + 250DF22C14C5190E0001DEFA /* RKOrderedDictionary.m in Sources */, + 49D2759F14C9EF1E0090845D /* ISO8601DateFormatter.m in Sources */, + 49D275AF14C9F3020090845D /* RKISO8601DateFormatter.m in Sources */, + 25B6E92014CF778D00B1E881 /* RKAbstractTableController.m in Sources */, + 25B6E92714CF778D00B1E881 /* RKControlTableItem.m in Sources */, + 25B6E92B14CF778D00B1E881 /* RKControlTableViewCell.m in Sources */, + 25B6E92F14CF778D00B1E881 /* RKFetchedResultsTableController.m in Sources */, + 25B6E93314CF778D00B1E881 /* RKForm.m in Sources */, + 25B6E93714CF778D00B1E881 /* RKFormSection.m in Sources */, + 25B6E93B14CF778D00B1E881 /* RKTableController.m in Sources */, + 25B6E93F14CF778D00B1E881 /* RKTableItem.m in Sources */, + 25B6E94314CF778D00B1E881 /* RKTableSection.m in Sources */, + 25B6E94714CF778D00B1E881 /* RKTableViewCellMapping.m in Sources */, + 25B6E94B14CF778D00B1E881 /* RKTableViewCellMappings.m in Sources */, + 25B6E95114CF778D00B1E881 /* UIView+FindFirstResponder.m in Sources */, + 25B6E95814CF7A1C00B1E881 /* RKErrors.m in Sources */, + 25B6E95E14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m in Sources */, + 25B6E9EB14CF940700B1E881 /* RKManagedObjectSearchEngine.m in Sources */, + 25B6E9EF14CF940700B1E881 /* RKSearchableManagedObject.m in Sources */, + 25B6E9F314CF940700B1E881 /* RKSearchWord.m in Sources */, + 25B6E9F714CF940700B1E881 /* RKSearchWordObserver.m in Sources */, + 25B6E9FF14CF943E00B1E881 /* RKCache.m in Sources */, + 25B6EA0314CF943E00B1E881 /* RKMutableBlockDictionary.m in Sources */, + 49A66B0E14CEFB0400A6F062 /* XMLReader.m in Sources */, + 49A66B1414CF03CA00A6F062 /* RKXMLParserXMLReader.m in Sources */, + 252EFB1D14D9A7CB004863C8 /* NSBundle+RKAdditions.m in Sources */, + 25FABED114E3796400E609E7 /* RKTestNotificationObserver.m in Sources */, + 25FABED814E37A2B00E609E7 /* RKTestResponseLoader.m in Sources */, + 25CA7A8F14EC570200888FF8 /* RKObjectMappingDefinition.m in Sources */, + 25055B8614EEF32A00B9C4DD /* RKMappingTest.m in Sources */, + 25055B8A14EEF32A00B9C4DD /* RKTestFactory.m in Sources */, + 25055B9114EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */, + 25EC1A2E14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */, + 25EC1A3B14F72B1300C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */, + 25EC1A3F14F72B3100C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */, + 25EC1A4314F72D0D00C3CF3F /* RKObjectMappingProvider+CoreData.m in Sources */, + 25EC1A4514F7393D00C3CF3F /* RKObjectMappingProviderContextEntry.m in Sources */, + 25EC1ABE14F8019F00C3CF3F /* RKRefreshGestureRecognizer.m in Sources */, + 25EC1AC214F8019F00C3CF3F /* RKRefreshTriggerView.m in Sources */, + 25EC1B3B14F84B5D00C3CF3F /* UIImage+RKAdditions.m in Sources */, + 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; }; @@ -2084,12 +2814,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 251610581456F2330060A5C5 /* RKManagedObjectLoaderSpec.m in Sources */, - 2516105A1456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m in Sources */, - 2516105C1456F2330060A5C5 /* RKManagedObjectMappingSpec.m in Sources */, - 2516105E1456F2330060A5C5 /* RKManagedObjectSpec.m in Sources */, - 251610601456F2330060A5C5 /* RKManagedObjectStoreSpec.m in Sources */, - 251610621456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m in Sources */, + 251610581456F2330060A5C5 /* RKManagedObjectLoaderTest.m in Sources */, + 2516105A1456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m in Sources */, + 2516105C1456F2330060A5C5 /* RKManagedObjectMappingTest.m in Sources */, + 251610601456F2330060A5C5 /* RKManagedObjectStoreTest.m in Sources */, + 251610621456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m in Sources */, 251610A21456F2330060A5C5 /* Data Model.xcdatamodel in Sources */, 251610A41456F2330060A5C5 /* RKCat.m in Sources */, 251610A61456F2330060A5C5 /* RKChild.m in Sources */, @@ -2098,59 +2827,73 @@ 251610AC1456F2330060A5C5 /* RKHuman.m in Sources */, 251610AE1456F2330060A5C5 /* RKMappableAssociation.m in Sources */, 251610B01456F2330060A5C5 /* RKMappableObject.m in Sources */, - 251610B21456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m in Sources */, - 251610B41456F2330060A5C5 /* RKObjectMapperSpecModel.m in Sources */, + 251610B21456F2330060A5C5 /* RKObjectLoaderTestResultModel.m in Sources */, + 251610B41456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */, 251610B61456F2330060A5C5 /* RKParent.m in Sources */, 251610B81456F2330060A5C5 /* RKResident.m in Sources */, - 251610BE1456F2330060A5C5 /* RKAuthenticationSpec.m in Sources */, - 251610C01456F2330060A5C5 /* RKClientSpec.m in Sources */, - 251610C21456F2330060A5C5 /* RKOAuthClientSpec.m in Sources */, - 251610C41456F2330060A5C5 /* RKParamsAttachmentSpec.m in Sources */, - 251610C61456F2330060A5C5 /* RKParamsSpec.m in Sources */, - 251610CA1456F2330060A5C5 /* RKRequestQueueSpec.m in Sources */, - 251610CC1456F2330060A5C5 /* RKRequestSpec.m in Sources */, - 251610CE1456F2330060A5C5 /* RKResponseSpec.m in Sources */, - 251610D01456F2330060A5C5 /* RKURLSpec.m in Sources */, - 251610D21456F2330060A5C5 /* RKDynamicObjectMappingSpec.m in Sources */, - 251610D41456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm in Sources */, - 251610D61456F2330060A5C5 /* RKObjectLoaderSpec.m in Sources */, - 251610D81456F2330060A5C5 /* RKObjectManagerSpec.m in Sources */, - 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenSpec.m in Sources */, - 251610DE1456F2330060A5C5 /* RKObjectMappingOperationSpec.m in Sources */, - 251610E01456F2330060A5C5 /* RKObjectMappingProviderSpec.m in Sources */, - 251610E21456F2330060A5C5 /* RKObjectMappingResultSpec.m in Sources */, - 251610E41456F2330060A5C5 /* RKObjectRouterSpec.m in Sources */, - 251610E61456F2330060A5C5 /* RKObjectSerializerSpec.m in Sources */, - 251610E81456F2330060A5C5 /* RKParserRegistrySpec.m in Sources */, - 251610F01456F2340060A5C5 /* RKSpecEnvironment.m in Sources */, - 251610F21456F2340060A5C5 /* RKSpecResponseLoader.m in Sources */, - 2516110E1456F2340060A5C5 /* NSDictionary+RKRequestSerializationSpec.m in Sources */, - 251611101456F2340060A5C5 /* NSStringRestKitSpec.m in Sources */, - 251611121456F2340060A5C5 /* RKDotNetDateFormatterSpec.m in Sources */, - 251611141456F2340060A5C5 /* RKJSONParserJSONKitSpec.m in Sources */, - 251611161456F2340060A5C5 /* RKPathMatcherSpec.m in Sources */, - 251611181456F2340060A5C5 /* RKXMLParserSpec.m in Sources */, - 25A341C2147C2F370009758D /* NSInvocation+OCMAdditions.m in Sources */, - 25A341C4147C2F370009758D /* NSMethodSignature+OCMAdditions.m in Sources */, - 25A341C6147C2F370009758D /* NSNotificationCenter+OCMAdditions.m in Sources */, - 25A341C8147C2F370009758D /* OCClassMockObject.m in Sources */, - 25A341CA147C2F370009758D /* OCMArg.m in Sources */, - 25A341CC147C2F370009758D /* OCMBlockCaller.m in Sources */, - 25A341CE147C2F370009758D /* OCMBoxedReturnValueProvider.m in Sources */, - 25A341D0147C2F370009758D /* OCMConstraint.m in Sources */, - 25A341D2147C2F370009758D /* OCMExceptionReturnValueProvider.m in Sources */, - 25A341D4147C2F370009758D /* OCMIndirectReturnValueProvider.m in Sources */, - 25A341D6147C2F370009758D /* OCMNotificationPoster.m in Sources */, - 25A341D8147C2F370009758D /* OCMObserverRecorder.m in Sources */, - 25A341DC147C2F370009758D /* OCMockObject.m in Sources */, - 25A341DE147C2F370009758D /* OCMockRecorder.m in Sources */, - 25A341E0147C2F370009758D /* OCMPassByRefSetter.m in Sources */, - 25A341E2147C2F370009758D /* OCMRealObjectForwarder.m in Sources */, - 25A341E4147C2F370009758D /* OCMReturnValueProvider.m in Sources */, - 25A341E6147C2F370009758D /* OCObserverMockObject.m in Sources */, - 25A341E8147C2F370009758D /* OCPartialMockObject.m in Sources */, - 25A341EA147C2F370009758D /* OCPartialMockRecorder.m in Sources */, - 25A341EC147C2F370009758D /* OCProtocolMockObject.m in Sources */, + 251610BE1456F2330060A5C5 /* RKAuthenticationTest.m in Sources */, + 251610C01456F2330060A5C5 /* RKClientTest.m in Sources */, + 251610C21456F2330060A5C5 /* RKOAuthClientTest.m in Sources */, + 251610C41456F2330060A5C5 /* RKParamsAttachmentTest.m in Sources */, + 251610C61456F2330060A5C5 /* RKParamsTest.m in Sources */, + 251610CA1456F2330060A5C5 /* RKRequestQueueTest.m in Sources */, + 251610CC1456F2330060A5C5 /* RKRequestTest.m in Sources */, + 251610CE1456F2330060A5C5 /* RKResponseTest.m in Sources */, + 251610D01456F2330060A5C5 /* RKURLTest.m in Sources */, + 251610D21456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */, + 251610D61456F2330060A5C5 /* RKObjectLoaderTest.m in Sources */, + 251610D81456F2330060A5C5 /* RKObjectManagerTest.m in Sources */, + 251610DC1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */, + 251610DE1456F2330060A5C5 /* RKObjectMappingOperationTest.m in Sources */, + 251610E01456F2330060A5C5 /* RKObjectMappingProviderTest.m in Sources */, + 251610E21456F2330060A5C5 /* RKObjectMappingResultTest.m in Sources */, + 251610E41456F2330060A5C5 /* RKObjectRouterTest.m in Sources */, + 251610E61456F2330060A5C5 /* RKObjectSerializerTest.m in Sources */, + 251610E81456F2330060A5C5 /* RKParserRegistryTest.m in Sources */, + 251610F01456F2340060A5C5 /* RKTestEnvironment.m in Sources */, + 2516110E1456F2340060A5C5 /* NSDictionary+RKRequestSerializationTest.m in Sources */, + 251611101456F2340060A5C5 /* NSStringRestKitTest.m in Sources */, + 251611121456F2340060A5C5 /* RKDotNetDateFormatterTest.m in Sources */, + 251611141456F2340060A5C5 /* RKJSONParserJSONKitTest.m in Sources */, + 251611161456F2340060A5C5 /* RKPathMatcherTest.m in Sources */, + 251611181456F2340060A5C5 /* RKXMLParserTest.m in Sources */, + 254A62C014AD591C00939BEE /* RKObjectPaginatorTest.m in Sources */, + 25B6E9A614CF829400B1E881 /* NSInvocation+OCMAdditions.m in Sources */, + 25B6E9A814CF829400B1E881 /* NSMethodSignature+OCMAdditions.m in Sources */, + 25B6E9AA14CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m in Sources */, + 25B6E9AC14CF829400B1E881 /* OCClassMockObject.m in Sources */, + 25B6E9AE14CF829400B1E881 /* OCMArg.m in Sources */, + 25B6E9B014CF829400B1E881 /* OCMBlockCaller.m in Sources */, + 25B6E9B214CF829400B1E881 /* OCMBoxedReturnValueProvider.m in Sources */, + 25B6E9B414CF829400B1E881 /* OCMConstraint.m in Sources */, + 25B6E9B614CF829400B1E881 /* OCMExceptionReturnValueProvider.m in Sources */, + 25B6E9B814CF829400B1E881 /* OCMIndirectReturnValueProvider.m in Sources */, + 25B6E9BA14CF829400B1E881 /* OCMNotificationPoster.m in Sources */, + 25B6E9BC14CF829400B1E881 /* OCMObserverRecorder.m in Sources */, + 25B6E9C014CF829400B1E881 /* OCMockObject.m in Sources */, + 25B6E9C214CF829400B1E881 /* OCMockRecorder.m in Sources */, + 25B6E9C414CF829400B1E881 /* OCMPassByRefSetter.m in Sources */, + 25B6E9C614CF829400B1E881 /* OCMRealObjectForwarder.m in Sources */, + 25B6E9C814CF829400B1E881 /* OCMReturnValueProvider.m in Sources */, + 25B6E9CA14CF829400B1E881 /* OCObserverMockObject.m in Sources */, + 25B6E9CC14CF829400B1E881 /* OCPartialMockObject.m in Sources */, + 25B6E9CE14CF829400B1E881 /* OCPartialMockRecorder.m in Sources */, + 25B6E9D014CF829400B1E881 /* OCProtocolMockObject.m in Sources */, + 25B6E9DB14CF912500B1E881 /* RKSearchable.m in Sources */, + 25B6E9DD14CF912500B1E881 /* RKTestAddress.m in Sources */, + 25B6E9DF14CF912500B1E881 /* RKTestUser.m in Sources */, + 252EFAFA14D8EAEC004863C8 /* RKEvent.m in Sources */, + 252EFB0814D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */, + 252EFB0A14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */, + 252EFB0D14D98F76004863C8 /* RKMutableBlockDictionaryTest.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; }; @@ -2202,21 +2945,58 @@ 25160F76145655D10060A5C5 /* RKManagedObjectStore.m in Sources */, 25160F78145655D10060A5C5 /* RKManagedObjectThreadSafeInvocation.m in Sources */, 25160F7A145655D10060A5C5 /* RKObjectPropertyInspector+CoreData.m in Sources */, - 25160F80145657650060A5C5 /* NSData+MD5.m in Sources */, - 25160F82145657650060A5C5 /* NSString+MD5.m in Sources */, - 25160F84145657650060A5C5 /* Errors.m in Sources */, + 25160F80145657650060A5C5 /* NSData+RKAdditions.m in Sources */, 25160F89145657650060A5C5 /* NSDictionary+RKAdditions.m in Sources */, - 25160F8B145657650060A5C5 /* NSString+RestKit.m in Sources */, - 25160F8D145657650060A5C5 /* NSURL+RestKit.m in Sources */, + 25160F8B145657650060A5C5 /* NSString+RKAdditions.m in Sources */, + 25160F8D145657650060A5C5 /* NSURL+RKAdditions.m in Sources */, 25160F8F1456576C0060A5C5 /* RKAlert.m in Sources */, 25160F911456576C0060A5C5 /* RKDotNetDateFormatter.m in Sources */, 25160F941456576C0060A5C5 /* RKLog.m in Sources */, 25160F961456576C0060A5C5 /* RKMIMETypes.m in Sources */, 25160F991456576C0060A5C5 /* RKPathMatcher.m in Sources */, 25160F9B1456576C0060A5C5 /* RKSearchEngine.m in Sources */, - 25160F9D145657720060A5C5 /* RKXMLParserLibXML.m in Sources */, 25160F9F1456577F0060A5C5 /* RKJSONParserJSONKit.m in Sources */, 25B408291491CDDC00F21111 /* RKDirectory.m in Sources */, + 254A62BC14AD544200939BEE /* RKObjectPaginator.m in Sources */, + 250DF22D14C5190E0001DEFA /* RKOrderedDictionary.m in Sources */, + 49D275A114C9EF1E0090845D /* ISO8601DateFormatter.m in Sources */, + 49D275B014C9F3020090845D /* RKISO8601DateFormatter.m in Sources */, + 25B6E95914CF7A1C00B1E881 /* RKErrors.m in Sources */, + 25B6E95F14CF7E3C00B1E881 /* RKDynamicObjectMappingMatcher.m in Sources */, + 25B6E9EC14CF940700B1E881 /* RKManagedObjectSearchEngine.m in Sources */, + 25B6E9F014CF940700B1E881 /* RKSearchableManagedObject.m in Sources */, + 25B6E9F414CF940700B1E881 /* RKSearchWord.m in Sources */, + 25B6E9F814CF940700B1E881 /* RKSearchWordObserver.m in Sources */, + 25B6EA0014CF943E00B1E881 /* RKCache.m in Sources */, + 25B6EA0414CF943E00B1E881 /* RKMutableBlockDictionary.m in Sources */, + 49A66B0F14CEFB0400A6F062 /* XMLReader.m in Sources */, + 49A66B1514CF03CA00A6F062 /* RKXMLParserXMLReader.m in Sources */, + 252EFB1E14D9A7CB004863C8 /* NSBundle+RKAdditions.m in Sources */, + 25FABED014E3796400E609E7 /* RKTestNotificationObserver.m in Sources */, + 25FABED914E37A2B00E609E7 /* RKTestResponseLoader.m in Sources */, + 25CA7A9014EC570200888FF8 /* RKObjectMappingDefinition.m in Sources */, + 25CA7A9114EC5C2D00888FF8 /* RKTestFixture.m in Sources */, + 25055B8714EEF32A00B9C4DD /* RKMappingTest.m in Sources */, + 25055B8B14EEF32A00B9C4DD /* RKTestFactory.m in Sources */, + 25055B9214EEF40000B9C4DD /* RKMappingTestExpectation.m in Sources */, + 25EC1A2F14F6FDAD00C3CF3F /* RKObjectManager+RKTableController.m in Sources */, + 25EC1A3C14F72B1400C3CF3F /* RKFetchRequestManagedObjectCache.m in Sources */, + 25EC1A4014F72B3300C3CF3F /* RKInMemoryManagedObjectCache.m in Sources */, + 25EC1A4614F7393E00C3CF3F /* RKObjectMappingProviderContextEntry.m in Sources */, + 25EC1ABF14F8019F00C3CF3F /* RKRefreshGestureRecognizer.m in Sources */, + 25EC1AC314F8019F00C3CF3F /* RKRefreshTriggerView.m in Sources */, + 25EC1B3C14F84B5D00C3CF3F /* UIImage+RKAdditions.m in Sources */, + 257ABAB315112DD500CCAA76 /* NSManagedObjectContext+RKAdditions.m in Sources */, + 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; }; @@ -2224,12 +3004,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 251610591456F2330060A5C5 /* RKManagedObjectLoaderSpec.m in Sources */, - 2516105B1456F2330060A5C5 /* RKManagedObjectMappingOperationSpec.m in Sources */, - 2516105D1456F2330060A5C5 /* RKManagedObjectMappingSpec.m in Sources */, - 2516105F1456F2330060A5C5 /* RKManagedObjectSpec.m in Sources */, - 251610611456F2330060A5C5 /* RKManagedObjectStoreSpec.m in Sources */, - 251610631456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationSpec.m in Sources */, + 251610591456F2330060A5C5 /* RKManagedObjectLoaderTest.m in Sources */, + 2516105B1456F2330060A5C5 /* RKManagedObjectMappingOperationTest.m in Sources */, + 2516105D1456F2330060A5C5 /* RKManagedObjectMappingTest.m in Sources */, + 251610611456F2330060A5C5 /* RKManagedObjectStoreTest.m in Sources */, + 251610631456F2330060A5C5 /* RKManagedObjectThreadSafeInvocationTest.m in Sources */, 251610A31456F2330060A5C5 /* Data Model.xcdatamodel in Sources */, 251610A51456F2330060A5C5 /* RKCat.m in Sources */, 251610A71456F2330060A5C5 /* RKChild.m in Sources */, @@ -2238,59 +3017,80 @@ 251610AD1456F2330060A5C5 /* RKHuman.m in Sources */, 251610AF1456F2330060A5C5 /* RKMappableAssociation.m in Sources */, 251610B11456F2330060A5C5 /* RKMappableObject.m in Sources */, - 251610B31456F2330060A5C5 /* RKObjectLoaderSpecResultModel.m in Sources */, - 251610B51456F2330060A5C5 /* RKObjectMapperSpecModel.m in Sources */, + 251610B31456F2330060A5C5 /* RKObjectLoaderTestResultModel.m in Sources */, + 251610B51456F2330060A5C5 /* RKObjectMapperTestModel.m in Sources */, 251610B71456F2330060A5C5 /* RKParent.m in Sources */, 251610B91456F2330060A5C5 /* RKResident.m in Sources */, - 251610BF1456F2330060A5C5 /* RKAuthenticationSpec.m in Sources */, - 251610C11456F2330060A5C5 /* RKClientSpec.m in Sources */, - 251610C31456F2330060A5C5 /* RKOAuthClientSpec.m in Sources */, - 251610C51456F2330060A5C5 /* RKParamsAttachmentSpec.m in Sources */, - 251610C71456F2330060A5C5 /* RKParamsSpec.m in Sources */, - 251610CB1456F2330060A5C5 /* RKRequestQueueSpec.m in Sources */, - 251610CD1456F2330060A5C5 /* RKRequestSpec.m in Sources */, - 251610CF1456F2330060A5C5 /* RKResponseSpec.m in Sources */, - 251610D11456F2330060A5C5 /* RKURLSpec.m in Sources */, - 251610D31456F2330060A5C5 /* RKDynamicObjectMappingSpec.m in Sources */, - 251610D51456F2330060A5C5 /* RKObjectiveCPlusPlusSpec.mm in Sources */, - 251610D71456F2330060A5C5 /* RKObjectLoaderSpec.m in Sources */, - 251610D91456F2330060A5C5 /* RKObjectManagerSpec.m in Sources */, - 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenSpec.m in Sources */, - 251610DF1456F2330060A5C5 /* RKObjectMappingOperationSpec.m in Sources */, - 251610E11456F2330060A5C5 /* RKObjectMappingProviderSpec.m in Sources */, - 251610E31456F2330060A5C5 /* RKObjectMappingResultSpec.m in Sources */, - 251610E51456F2330060A5C5 /* RKObjectRouterSpec.m in Sources */, - 251610E71456F2330060A5C5 /* RKObjectSerializerSpec.m in Sources */, - 251610E91456F2330060A5C5 /* RKParserRegistrySpec.m in Sources */, - 251610F11456F2340060A5C5 /* RKSpecEnvironment.m in Sources */, - 251610F31456F2340060A5C5 /* RKSpecResponseLoader.m in Sources */, - 2516110F1456F2340060A5C5 /* NSDictionary+RKRequestSerializationSpec.m in Sources */, - 251611111456F2340060A5C5 /* NSStringRestKitSpec.m in Sources */, - 251611131456F2340060A5C5 /* RKDotNetDateFormatterSpec.m in Sources */, - 251611151456F2340060A5C5 /* RKJSONParserJSONKitSpec.m in Sources */, - 251611171456F2340060A5C5 /* RKPathMatcherSpec.m in Sources */, - 251611191456F2340060A5C5 /* RKXMLParserSpec.m in Sources */, - 25A341C3147C2F370009758D /* NSInvocation+OCMAdditions.m in Sources */, - 25A341C5147C2F370009758D /* NSMethodSignature+OCMAdditions.m in Sources */, - 25A341C7147C2F370009758D /* NSNotificationCenter+OCMAdditions.m in Sources */, - 25A341C9147C2F370009758D /* OCClassMockObject.m in Sources */, - 25A341CB147C2F370009758D /* OCMArg.m in Sources */, - 25A341CD147C2F370009758D /* OCMBlockCaller.m in Sources */, - 25A341CF147C2F370009758D /* OCMBoxedReturnValueProvider.m in Sources */, - 25A341D1147C2F370009758D /* OCMConstraint.m in Sources */, - 25A341D3147C2F370009758D /* OCMExceptionReturnValueProvider.m in Sources */, - 25A341D5147C2F370009758D /* OCMIndirectReturnValueProvider.m in Sources */, - 25A341D7147C2F370009758D /* OCMNotificationPoster.m in Sources */, - 25A341D9147C2F370009758D /* OCMObserverRecorder.m in Sources */, - 25A341DD147C2F370009758D /* OCMockObject.m in Sources */, - 25A341DF147C2F370009758D /* OCMockRecorder.m in Sources */, - 25A341E1147C2F370009758D /* OCMPassByRefSetter.m in Sources */, - 25A341E3147C2F370009758D /* OCMRealObjectForwarder.m in Sources */, - 25A341E5147C2F370009758D /* OCMReturnValueProvider.m in Sources */, - 25A341E7147C2F370009758D /* OCObserverMockObject.m in Sources */, - 25A341E9147C2F370009758D /* OCPartialMockObject.m in Sources */, - 25A341EB147C2F370009758D /* OCPartialMockRecorder.m in Sources */, - 25A341ED147C2F370009758D /* OCProtocolMockObject.m in Sources */, + 251610BF1456F2330060A5C5 /* RKAuthenticationTest.m in Sources */, + 251610C11456F2330060A5C5 /* RKClientTest.m in Sources */, + 251610C31456F2330060A5C5 /* RKOAuthClientTest.m in Sources */, + 251610C51456F2330060A5C5 /* RKParamsAttachmentTest.m in Sources */, + 251610C71456F2330060A5C5 /* RKParamsTest.m in Sources */, + 251610CB1456F2330060A5C5 /* RKRequestQueueTest.m in Sources */, + 251610CD1456F2330060A5C5 /* RKRequestTest.m in Sources */, + 251610CF1456F2330060A5C5 /* RKResponseTest.m in Sources */, + 251610D11456F2330060A5C5 /* RKURLTest.m in Sources */, + 251610D31456F2330060A5C5 /* RKDynamicObjectMappingTest.m in Sources */, + 251610D71456F2330060A5C5 /* RKObjectLoaderTest.m in Sources */, + 251610D91456F2330060A5C5 /* RKObjectManagerTest.m in Sources */, + 251610DD1456F2330060A5C5 /* RKObjectMappingNextGenTest.m in Sources */, + 251610DF1456F2330060A5C5 /* RKObjectMappingOperationTest.m in Sources */, + 251610E11456F2330060A5C5 /* RKObjectMappingProviderTest.m in Sources */, + 251610E31456F2330060A5C5 /* RKObjectMappingResultTest.m in Sources */, + 251610E51456F2330060A5C5 /* RKObjectRouterTest.m in Sources */, + 251610E71456F2330060A5C5 /* RKObjectSerializerTest.m in Sources */, + 251610E91456F2330060A5C5 /* RKParserRegistryTest.m in Sources */, + 251610F11456F2340060A5C5 /* RKTestEnvironment.m in Sources */, + 2516110F1456F2340060A5C5 /* NSDictionary+RKRequestSerializationTest.m in Sources */, + 251611111456F2340060A5C5 /* NSStringRestKitTest.m in Sources */, + 251611131456F2340060A5C5 /* RKDotNetDateFormatterTest.m in Sources */, + 251611151456F2340060A5C5 /* RKJSONParserJSONKitTest.m in Sources */, + 251611171456F2340060A5C5 /* RKPathMatcherTest.m in Sources */, + 251611191456F2340060A5C5 /* RKXMLParserTest.m in Sources */, + 254A62C114AD591C00939BEE /* RKObjectPaginatorTest.m in Sources */, + 25B6E9A714CF829400B1E881 /* NSInvocation+OCMAdditions.m in Sources */, + 25B6E9A914CF829400B1E881 /* NSMethodSignature+OCMAdditions.m in Sources */, + 25B6E9AB14CF829400B1E881 /* NSNotificationCenter+OCMAdditions.m in Sources */, + 25B6E9AD14CF829400B1E881 /* OCClassMockObject.m in Sources */, + 25B6E9AF14CF829400B1E881 /* OCMArg.m in Sources */, + 25B6E9B114CF829400B1E881 /* OCMBlockCaller.m in Sources */, + 25B6E9B314CF829400B1E881 /* OCMBoxedReturnValueProvider.m in Sources */, + 25B6E9B514CF829400B1E881 /* OCMConstraint.m in Sources */, + 25B6E9B714CF829400B1E881 /* OCMExceptionReturnValueProvider.m in Sources */, + 25B6E9B914CF829400B1E881 /* OCMIndirectReturnValueProvider.m in Sources */, + 25B6E9BB14CF829400B1E881 /* OCMNotificationPoster.m in Sources */, + 25B6E9BD14CF829400B1E881 /* OCMObserverRecorder.m in Sources */, + 25B6E9C114CF829400B1E881 /* OCMockObject.m in Sources */, + 25B6E9C314CF829400B1E881 /* OCMockRecorder.m in Sources */, + 25B6E9C514CF829400B1E881 /* OCMPassByRefSetter.m in Sources */, + 25B6E9C714CF829400B1E881 /* OCMRealObjectForwarder.m in Sources */, + 25B6E9C914CF829400B1E881 /* OCMReturnValueProvider.m in Sources */, + 25B6E9CB14CF829400B1E881 /* OCObserverMockObject.m in Sources */, + 25B6E9CD14CF829400B1E881 /* OCPartialMockObject.m in Sources */, + 25B6E9CF14CF829400B1E881 /* OCPartialMockRecorder.m in Sources */, + 25B6E9D114CF829400B1E881 /* OCProtocolMockObject.m in Sources */, + 25B6E9DC14CF912500B1E881 /* RKSearchable.m in Sources */, + 25B6E9DE14CF912500B1E881 /* RKTestAddress.m in Sources */, + 25B6E9E014CF912500B1E881 /* RKTestUser.m in Sources */, + 252EFAFB14D8EAEC004863C8 /* RKEvent.m in Sources */, + 252EFB0914D98F4D004863C8 /* RKSearchableManagedObjectTest.m in Sources */, + 252EFB0B14D98F4D004863C8 /* RKSearchWordObserverTest.m in Sources */, + 252EFB0E14D98F76004863C8 /* RKMutableBlockDictionaryTest.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; + }; + 259C301215128079003066A2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2310,10 +3110,10 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 25A34191147C2F370009758D /* InfoPlist.strings */ = { + 25B6E97B14CF829400B1E881 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( - 25A34192147C2F370009758D /* en */, + 25B6E97C14CF829400B1E881 /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -2372,11 +3172,13 @@ GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; HEADER_SEARCH_PATHS = "${SDKROOT}/usr/include/libxml2"; IPHONEOS_DEPLOYMENT_TARGET = 4.0; + OBJROOT = "$(SRCROOT)/Build"; OTHER_LDFLAGS = "-ObjC"; PRIVATE_HEADERS_FOLDER_PATH = "$(PUBLIC_HEADERS_FOLDER_PATH)/Private"; PRODUCT_NAME = "$(TARGET_NAME)"; - PUBLIC_HEADERS_FOLDER_PATH = include/RestKit; + PUBLIC_HEADERS_FOLDER_PATH = ../../Headers/RestKit; SKIP_INSTALL = YES; + SYMROOT = "$(SRCROOT)/Build/Products"; }; name = Debug; }; @@ -2391,11 +3193,13 @@ GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; HEADER_SEARCH_PATHS = "${SDKROOT}/usr/include/libxml2"; IPHONEOS_DEPLOYMENT_TARGET = 4.0; + OBJROOT = "$(SRCROOT)/Build"; OTHER_LDFLAGS = "-ObjC"; PRIVATE_HEADERS_FOLDER_PATH = "$(PUBLIC_HEADERS_FOLDER_PATH)/Private"; PRODUCT_NAME = "$(TARGET_NAME)"; - PUBLIC_HEADERS_FOLDER_PATH = include/RestKit; + PUBLIC_HEADERS_FOLDER_PATH = ../../Headers/RestKit; SKIP_INSTALL = YES; + SYMROOT = "$(SRCROOT)/Build/Products"; }; name = Release; }; @@ -2403,17 +3207,20 @@ isa = XCBuildConfiguration; buildSettings = { FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - "\"$(SRCROOT)/Specs/Runner\"", - "\"$(SRCROOT)/Specs/Runner/OCHamcrest\"", + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "\"$(SRCROOT)/Tests/Vendor\"", + "\"$(SRCROOT)/Tests/Vendor/OCHamcrest\"", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; - HEADER_SEARCH_PATHS = "$(SRCROOT)/Specs/Runner/OCMock"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Tests/Vendor/OCMock\""; INFOPLIST_FILE = "Resources/PLISTs/RestKitTests-Info.plist"; + LIBRARY_SEARCH_PATHS = "${CONFIGURATION_BUILD_DIR}"; + OBJROOT = "$(SRCROOT)/Build"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + SYMROOT = "$(SRCROOT)/Build/Products"; WRAPPER_EXTENSION = octest; }; name = Debug; @@ -2422,17 +3229,20 @@ isa = XCBuildConfiguration; buildSettings = { FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - "\"$(SRCROOT)/Specs/Runner\"", - "\"$(SRCROOT)/Specs/Runner/OCHamcrest\"", + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "\"$(SRCROOT)/Tests/Vendor\"", + "\"$(SRCROOT)/Tests/Vendor/OCHamcrest\"", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; - HEADER_SEARCH_PATHS = "$(SRCROOT)/Specs/Runner/OCMock"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Tests/Vendor/OCMock\""; INFOPLIST_FILE = "Resources/PLISTs/RestKitTests-Info.plist"; + LIBRARY_SEARCH_PATHS = "${CONFIGURATION_BUILD_DIR}"; + OBJROOT = "$(SRCROOT)/Build"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + SYMROOT = "$(SRCROOT)/Build/Products"; WRAPPER_EXTENSION = octest; }; name = Release; @@ -2454,10 +3264,12 @@ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; HEADER_SEARCH_PATHS = "${SDKROOT}/usr/include/libxml2"; INFOPLIST_FILE = "Resources/PLISTs/RestKitFramework-Info.plist"; + INSTALL_PATH = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.7; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = RestKit; SDKROOT = macosx; + SKIP_INSTALL = YES; WRAPPER_EXTENSION = framework; }; name = Debug; @@ -2480,9 +3292,11 @@ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; HEADER_SEARCH_PATHS = "${SDKROOT}/usr/include/libxml2"; INFOPLIST_FILE = "Resources/PLISTs/RestKitFramework-Info.plist"; + INSTALL_PATH = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.7; PRODUCT_NAME = RestKit; SDKROOT = macosx; + SKIP_INSTALL = YES; WRAPPER_EXTENSION = framework; }; name = Release; @@ -2492,19 +3306,19 @@ buildSettings = { ARCHS = "$(ARCHS_STANDARD_64_BIT)"; FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - "\"$(SRCROOT)/Specs/Runner\"", - "\"$(SRCROOT)/Specs/Runner/OCHamcrest\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "\"$(SRCROOT)/Tests/Vendor\"", + "\"$(SRCROOT)/Tests/Vendor/OCHamcrest\"", ); GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - HEADER_SEARCH_PATHS = "$(SRCROOT)/Specs/Runner/OCMock"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Tests/Vendor/OCMock\""; INFOPLIST_FILE = "Resources/PLISTs/RestKitFrameworkTests-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/Specs/Runner/OCMock\"", + "\"$(SRCROOT)/Tests/Vendor/OCMock\"", ); MACOSX_DEPLOYMENT_TARGET = 10.7; ONLY_ACTIVE_ARCH = YES; @@ -2520,19 +3334,19 @@ ARCHS = "$(ARCHS_STANDARD_64_BIT)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - "\"$(SRCROOT)/Specs/Runner\"", - "\"$(SRCROOT)/Specs/Runner/OCHamcrest\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "\"$(SRCROOT)/Tests/Vendor\"", + "\"$(SRCROOT)/Tests/Vendor/OCHamcrest\"", ); GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Code/Support/RestKit-Prefix.pch"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - HEADER_SEARCH_PATHS = "$(SRCROOT)/Specs/Runner/OCMock"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Tests/Vendor/OCMock\""; INFOPLIST_FILE = "Resources/PLISTs/RestKitFrameworkTests-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/Specs/Runner/OCMock\"", + "\"$(SRCROOT)/Tests/Vendor/OCMock\"", ); MACOSX_DEPLOYMENT_TARGET = 10.7; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2541,6 +3355,44 @@ }; name = Release; }; + 259C302015128079003066A2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Resources/RestKitResources-Prefix.pch"; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + INFOPLIST_FILE = "Resources/PLISTs/RestKitResources-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 259C302115128079003066A2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Resources/RestKitResources-Prefix.pch"; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + INFOPLIST_FILE = "Resources/PLISTs/RestKitResources-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + MACOSX_DEPLOYMENT_TARGET = 10.7; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2589,7 +3441,29 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 259C301F15128079003066A2 /* Build configuration list for PBXNativeTarget "RestKitResources" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 259C302015128079003066A2 /* Debug */, + 259C302115128079003066A2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 25EC1ADB14F8022600C3CF3F /* RestKitCoreData.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 25EC1ADC14F8022600C3CF3F /* RestKitCoreData.xcdatamodel */, + ); + currentVersion = 25EC1ADC14F8022600C3CF3F /* RestKitCoreData.xcdatamodel */; + path = RestKitCoreData.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 25160D0D14564E810060A5C5 /* Project object */; } diff --git a/RestKitResources.bundle/Contents/Info.plist b/RestKitResources.bundle/Contents/Info.plist new file mode 100644 index 0000000000..b7c33a135e --- /dev/null +++ b/RestKitResources.bundle/Contents/Info.plist @@ -0,0 +1,60 @@ + + + + + BuildMachineOSBuild + 11D50b + CFBundleDevelopmentRegion + English + CFBundleExecutable + RestKitResources + CFBundleIdentifier + org.restkit.RestKitResources + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + RestKitResources + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + CFPlugInDynamicRegisterFunction + + CFPlugInDynamicRegistration + NO + CFPlugInFactories + + 00000000-0000-0000-0000-000000000000 + MyFactoryFunction + + CFPlugInTypes + + 00000000-0000-0000-0000-000000000000 + + 00000000-0000-0000-0000-000000000000 + + + CFPlugInUnloadFunction + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 4E1019 + DTPlatformVersion + GM + DTSDKBuild + 11D50a + DTSDKName + macosx10.7 + DTXcode + 0431 + DTXcodeBuild + 4E1019 + NSHumanReadableCopyright + Copyright © 2012 RestKit. All rights reserved. + + diff --git a/RestKitResources.bundle/Contents/Resources/blackArrow.png b/RestKitResources.bundle/Contents/Resources/blackArrow.png new file mode 100644 index 0000000000..6d2ffbc3a2 Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/blackArrow.png differ diff --git a/RestKitResources.bundle/Contents/Resources/blackArrow@2x.png b/RestKitResources.bundle/Contents/Resources/blackArrow@2x.png new file mode 100644 index 0000000000..ec4ec0e007 Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/blackArrow@2x.png differ diff --git a/RestKitResources.bundle/Contents/Resources/blueArrow.png b/RestKitResources.bundle/Contents/Resources/blueArrow.png new file mode 100644 index 0000000000..d6240d4a22 Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/blueArrow.png differ diff --git a/RestKitResources.bundle/Contents/Resources/blueArrow@2x.png b/RestKitResources.bundle/Contents/Resources/blueArrow@2x.png new file mode 100644 index 0000000000..a6a7e55c9b Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/blueArrow@2x.png differ diff --git a/RestKitResources.bundle/Contents/Resources/grayArrow.png b/RestKitResources.bundle/Contents/Resources/grayArrow.png new file mode 100644 index 0000000000..d69e9d94e6 Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/grayArrow.png differ diff --git a/RestKitResources.bundle/Contents/Resources/grayArrow@2x.png b/RestKitResources.bundle/Contents/Resources/grayArrow@2x.png new file mode 100644 index 0000000000..9d42357cf6 Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/grayArrow@2x.png differ diff --git a/RestKitResources.bundle/Contents/Resources/whiteArrow.png b/RestKitResources.bundle/Contents/Resources/whiteArrow.png new file mode 100644 index 0000000000..4bf569025c Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/whiteArrow.png differ diff --git a/RestKitResources.bundle/Contents/Resources/whiteArrow@2x.png b/RestKitResources.bundle/Contents/Resources/whiteArrow@2x.png new file mode 100644 index 0000000000..70569746db Binary files /dev/null and b/RestKitResources.bundle/Contents/Resources/whiteArrow@2x.png differ diff --git a/Scripts/CopyHeadersToLegacyBuildDir.command b/Scripts/CopyHeadersToLegacyBuildDir.command deleted file mode 100644 index e9f2b318d6..0000000000 --- a/Scripts/CopyHeadersToLegacyBuildDir.command +++ /dev/null @@ -1,11 +0,0 @@ -# This script copies the headers from the configuration -# build directory to their legacy location at Build/RestKit -# under the project root. This is the include path for Xcode -# 3 projects and Xcode 4 projects not using derived data - -IFS=$'\n' -if [ -d "${TARGET_BUILD_DIR}/include/RestKit" ]; then - rsync -av --delete "${TARGET_BUILD_DIR}/include/RestKit" "${SOURCE_ROOT}/Build" -else - echo "Target Build Directory '${TARGET_BUILD_DIR}' do not exist, skipping..." -fi diff --git a/Scripts/Protect.command b/Scripts/Protect.command deleted file mode 100644 index e6e631740e..0000000000 --- a/Scripts/Protect.command +++ /dev/null @@ -1,15 +0,0 @@ -# Protect the copied header files from being modified. This is done in an attempt to avoid -# accidentally editing the copied headers. - -# Ignore whitespace characters in paths -IFS=$'\n' - -if [ -d "${TARGET_BUILD_DIR}/include/RestKit" ]; then - cd ${TARGET_BUILD_DIR}/include/RestKit - - find * -name '*.h' | xargs chmod a-w -else - echo "Target Build Directory '${TARGET_BUILD_DIR}' do not exist, skipping..." -fi - -exit 0 diff --git a/Specs/CoreData/RKManagedObjectLoaderSpec.m b/Specs/CoreData/RKManagedObjectLoaderSpec.m deleted file mode 100644 index 401cd22afc..0000000000 --- a/Specs/CoreData/RKManagedObjectLoaderSpec.m +++ /dev/null @@ -1,238 +0,0 @@ -// -// RKManagedObjectLoaderSpec.m -// RestKit -// -// Created by Blake Watters on 4/28/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKManagedObjectLoader.h" -#import "RKManagedObjectMapping.h" -#import "RKHuman.h" -#import "RKCat.h" -#import "NSManagedObject+ActiveRecord.h" - - -/* - * A special mock for testing the managed object cache. - * We are not using OCMock here so we can test the case - * where optional protocol selectors are not defined. - */ -@interface TestObjectCache : NSObject { - -} - -@end - -@implementation TestObjectCache -- (NSArray *)fetchRequestsForResourcePath:(NSString *)resourcePath -{ - return [NSArray arrayWithObject:[RKHuman fetchRequest]]; -} -@end - -@interface RKManagedObjectLoaderSpec : RKSpec { - -} - -@end - -@interface TestCacheRemoveOddOrphans : TestObjectCache { -} -@end - -@implementation TestCacheRemoveOddOrphans -- (BOOL)shouldDeleteOrphanedObject:(NSManagedObject *)managedObject -{ - RKHuman* human = (RKHuman*)managedObject; - return [human.railsID integerValue] % 2 == 0 ? NO : YES; -} -@end - -@implementation RKManagedObjectLoaderSpec - -- (void)testShouldDeleteObjectFromLocalStoreOnDELETE { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - objectManager.objectStore = store; - RKHuman* human = [RKHuman object]; - human.name = @"Blake Watters"; - human.railsID = [NSNumber numberWithInt:1]; - [objectManager.objectStore save]; - - RKObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithResourcePath:@"/humans/1" objectManager:objectManager delegate:responseLoader]; - objectLoader.method = RKRequestMethodDELETE; - objectLoader.objectMapping = mapping; - objectLoader.targetObject = human; - [objectLoader send]; - [responseLoader waitForResponse]; - assertThatBool([human isDeleted], equalToBool(YES)); -} - -- (void)testShouldLoadAnObjectWithAToOneRelationship { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - objectManager.objectStore = store; - - RKObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - [humanMapping mapAttributes:@"name", nil]; - RKObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - [catMapping mapAttributes:@"name", nil]; - [humanMapping mapKeyPath:@"favorite_cat" toRelationship:@"favoriteCat" withMapping:catMapping]; - [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithResourcePath:@"/JSON/humans/with_to_one_relationship.json" objectManager:objectManager delegate:responseLoader]; - [objectLoader send]; - [responseLoader waitForResponse]; - RKHuman* human = [responseLoader.objects lastObject]; - assertThat(human, isNot(nilValue())); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldDeleteObjectsMissingFromPayloadReturnedByObjectCache { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman"]; - [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; - [humanMapping mapAttributes:@"name", nil]; - humanMapping.primaryKeyAttribute = @"railsID"; - - // Create 3 objects, we will expect 2 after the load - [RKHuman truncateAll]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(0))); - RKHuman* blake = [RKHuman createEntity]; - blake.railsID = [NSNumber numberWithInt:123]; - RKHuman* other = [RKHuman createEntity]; - other.railsID = [NSNumber numberWithInt:456]; - RKHuman* deleteMe = [RKHuman createEntity]; - deleteMe.railsID = [NSNumber numberWithInt:9999]; - [store save]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(3))); - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; - RKSpecStubNetworkAvailability(YES); - objectManager.objectStore = store; - objectManager.objectStore.managedObjectCache = [[[TestObjectCache alloc] init] autorelease]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 25; - RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithResourcePath:@"/JSON/humans/all.json" objectManager:objectManager delegate:responseLoader]; - [objectLoader send]; - [responseLoader waitForResponse]; - - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(2))); - assertThatBool([blake isDeleted], is(equalToBool(NO))); - assertThatBool([other isDeleted], is(equalToBool(NO))); - assertThatBool([deleteMe isDeleted], is(equalToBool(YES))); -} - -- (void)testShouldNotDeleteOrphansFromManagedObjectCache -{ - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman"]; - [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; - [humanMapping mapAttributes:@"name", nil]; - humanMapping.primaryKeyAttribute = @"railsID"; - - // Create 4 objects, we will expect 4 after the load - [RKHuman truncateAll]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(0))); - RKHuman* blake = [RKHuman createEntity]; - blake.railsID = [NSNumber numberWithInt:123]; - RKHuman* other = [RKHuman createEntity]; - other.railsID = [NSNumber numberWithInt:456]; - RKHuman* deleteOdd = [RKHuman createEntity]; - deleteOdd.railsID = [NSNumber numberWithInt:9999]; - RKHuman* doNotDeleteMe = [RKHuman createEntity]; - doNotDeleteMe.railsID = [NSNumber numberWithInt:1000]; - [store save]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(4))); - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; - RKSpecStubNetworkAvailability(YES); - objectManager.objectStore = store; - - id mockObjectCache = [OCMockObject mockForProtocol:@protocol(RKManagedObjectCache)]; - NSArray* fetchRequests = [NSArray arrayWithObject:[RKHuman fetchRequest]]; - [[[mockObjectCache expect] andReturn:fetchRequests] fetchRequestsForResourcePath:OCMOCK_ANY]; - const BOOL no = NO; - [[[mockObjectCache stub] andReturnValue:OCMOCK_VALUE(no)] shouldDeleteOrphanedObject:OCMOCK_ANY]; - objectManager.objectStore.managedObjectCache = mockObjectCache; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 25; - RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithResourcePath:@"/JSON/humans/all.json" objectManager:objectManager delegate:responseLoader]; - [objectLoader send]; - [responseLoader waitForResponse]; - - NSArray* humans = [RKHuman findAll]; - assertThatUnsignedInteger([humans count], is(equalToInt(4))); - assertThatBool([blake isDeleted], is(equalToBool(NO))); - assertThatBool([other isDeleted], is(equalToBool(NO))); - assertThatBool([deleteOdd isDeleted], is(equalToBool(NO))); - assertThatBool([doNotDeleteMe isDeleted], is(equalToBool(NO))); -} - -- (void)testShouldNotDeleteOddOrphansFromManagedObjectCache -{ - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman"]; - [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; - [humanMapping mapAttributes:@"name", nil]; - humanMapping.primaryKeyAttribute = @"railsID"; - - // Create 4 objects, we will expect 4 after the load - [RKHuman truncateAll]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(0))); - RKHuman* blake = [RKHuman createEntity]; - blake.railsID = [NSNumber numberWithInt:123]; - RKHuman* other = [RKHuman createEntity]; - other.railsID = [NSNumber numberWithInt:456]; - RKHuman* deleteOdd = [RKHuman createEntity]; - deleteOdd.railsID = [NSNumber numberWithInt:9999]; - RKHuman* doNotDeleteMe = [RKHuman createEntity]; - doNotDeleteMe.railsID = [NSNumber numberWithInt:1000]; - [store save]; - assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(4))); - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; - RKSpecStubNetworkAvailability(YES); - objectManager.objectStore = store; - objectManager.objectStore.managedObjectCache = [[[TestCacheRemoveOddOrphans alloc] init] autorelease]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 25; - RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithResourcePath:@"/JSON/humans/all.json" objectManager:objectManager delegate:responseLoader]; - [objectLoader send]; - [responseLoader waitForResponse]; - - NSArray* humans = [RKHuman findAll]; - assertThatUnsignedInteger([humans count], is(equalToInt(3))); - assertThatBool([blake isDeleted], is(equalToBool(NO))); - assertThatBool([other isDeleted], is(equalToBool(NO))); - assertThatBool([deleteOdd isDeleted], is(equalToBool(YES))); - assertThatBool([doNotDeleteMe isDeleted], is(equalToBool(NO))); -} - - -@end diff --git a/Specs/CoreData/RKManagedObjectMappingOperationSpec.m b/Specs/CoreData/RKManagedObjectMappingOperationSpec.m deleted file mode 100644 index dd87c94ee0..0000000000 --- a/Specs/CoreData/RKManagedObjectMappingOperationSpec.m +++ /dev/null @@ -1,225 +0,0 @@ -// -// RKManagedObjectMappingOperationSpec.m -// RestKit -// -// Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKManagedObjectMapping.h" -#import "RKManagedObjectMappingOperation.h" -#import "RKCat.h" -#import "RKHuman.h" -#import "RKChild.h" -#import "RKParent.h" - -@interface RKManagedObjectMappingOperationSpec : RKSpec { - -} - -@end - -@implementation RKManagedObjectMappingOperationSpec - -- (void)testShouldOverloadInitializationOfRKObjectMappingOperationToReturnInstancesOfRKManagedObjectMappingOperationWhenAppropriate { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* managedMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - NSDictionary* sourceObject = [NSDictionary dictionary]; - RKHuman* human = [RKHuman createEntity]; - RKObjectMappingOperation* operation = [RKObjectMappingOperation mappingOperationFromObject:sourceObject toObject:human withMapping:managedMapping]; - assertThat(operation, is(instanceOf([RKManagedObjectMappingOperation class]))); -} - -- (void)testShouldOverloadInitializationOfRKObjectMappingOperationButReturnUnmanagedMappingOperationWhenAppropriate { - RKObjectMapping* vanillaMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; - NSDictionary* sourceObject = [NSDictionary dictionary]; - NSMutableDictionary* destinationObject = [NSMutableDictionary dictionary]; - RKObjectMappingOperation* operation = [RKObjectMappingOperation mappingOperationFromObject:sourceObject toObject:destinationObject withMapping:vanillaMapping]; - assertThat(operation, is(instanceOf([RKObjectMappingOperation class]))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKey { - RKManagedObjectStore* objectStore = RKSpecNewManagedObjectStore(); - - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - catMapping.primaryKeyAttribute = @"railsID"; - [catMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - 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]; - - 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]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPaths { - RKManagedObjectStore* objectStore = RKSpecNewManagedObjectStore(); - - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - catMapping.primaryKeyAttribute = @"railsID"; - [catMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - humanMapping.primaryKeyAttribute = @"railsID"; - [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; - [humanMapping mapKeyPath:@"favorite_cat" toRelationship:@"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]; - - 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]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.favoriteCat, isNot(nilValue())); - assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); -} - -- (void)testShouldLoadNestedHasManyRelationship { - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - catMapping.primaryKeyAttribute = @"railsID"; - [catMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - humanMapping.primaryKeyAttribute = @"railsID"; - [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; - [humanMapping hasMany:@"cats" 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]; - RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); -} - -- (void)testShouldMapNullToAHasManyRelationship { - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - [catMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; - [humanMapping hasMany:@"cats" withMapping:catMapping]; - - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"cats", [NSNull null], nil]; - RKHuman* human = [RKHuman object]; - RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(human.cats, is(empty())); -} - -- (void)testShouldLoadNestedHasManyRelationshipWithoutABackingClass { - RKManagedObjectStore* objectStore = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* cloudMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKCloud"]; - [cloudMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* stormMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKStorm"]; - [stormMapping mapAttributes:@"name", @"favoriteCatID", nil]; - [stormMapping hasMany:@"clouds" withMapping:cloudMapping]; - - NSArray* cloudsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Nimbus" forKey:@"name"]]; - NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Hurricane", @"clouds", cloudsData, nil]; - NSEntityDescription* entity = [NSEntityDescription entityForName:@"RKStorm" inManagedObjectContext:objectStore.managedObjectContext]; - NSManagedObject* storm = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:objectStore.managedObjectContext]; - RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:storm mapping:stormMapping]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); -} - -- (void)testShouldConnectManyToManyRelationships { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class]]; - childMapping.primaryKeyAttribute = @"railsID"; - [childMapping mapAttributes:@"name", nil]; - - RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class]]; - parentMapping.primaryKeyAttribute = @"railsID"; - [parentMapping mapAttributes:@"name", @"age", nil]; - [parentMapping hasMany:@"children" withMapping:childMapping]; - - NSArray* childMappableData = [NSArray arrayWithObjects:[NSDictionary dictionaryWithKeysAndObjects:@"name", @"Maya", nil], - [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Brady", nil], nil]; - NSDictionary* parentMappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Win", - @"age", [NSNumber numberWithInt:34], - @"children", childMappableData, nil]; - RKParent* parent = [RKParent object]; - RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:parentMappableData destinationObject:parent mapping:parentMapping]; - NSError* error = nil; - BOOL success = [operation performMapping:&error]; - assertThatBool(success, is(equalToBool(YES))); - assertThat(parent.children, isNot(nilValue())); - assertThatUnsignedInteger([parent.children count], is(equalToInt(2))); - assertThat([[parent.children anyObject] parents], isNot(nilValue())); - assertThatBool([[[parent.children anyObject] parents] containsObject:parent], is(equalToBool(YES))); - assertThatUnsignedInteger([[[parent.children anyObject] parents] count], is(equalToInt(1))); -} - -- (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class]]; - [parentMapping mapAttributes:@"parentID", nil]; - parentMapping.primaryKeyAttribute = @"parentID"; - - RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class]]; - [childMapping mapAttributes:@"fatherID", nil]; - [childMapping mapRelationship:@"father" withMapping:parentMapping]; - [childMapping connectRelationship:@"father" withObjectForPrimaryKeyAttribute:@"fatherID"]; - - 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 setMapping:parentMapping forKeyPath:@"parents"]; - [mappingProvider setMapping:childMapping forKeyPath:@"children"]; - - NSDictionary *JSON = RKSpecParseFixture(@"ConnectingParents.json"); - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); - RKLogConfigureByName("RestKit/CoreData", RKLogLevelTrace); - RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; - RKObjectMappingResult *result = [mapper performMapping]; - NSArray *children = [[result asDictionary] valueForKey:@"children"]; - assertThat(children, hasCountOf(1)); - RKChild *child = [children lastObject]; - assertThat(child.father, is(notNilValue())); -} - -@end diff --git a/Specs/CoreData/RKManagedObjectMappingSpec.m b/Specs/CoreData/RKManagedObjectMappingSpec.m deleted file mode 100644 index 59de8042bc..0000000000 --- a/Specs/CoreData/RKManagedObjectMappingSpec.m +++ /dev/null @@ -1,158 +0,0 @@ -// -// RKManagedObjectMappingSpec.m -// RestKit -// -// Created by Blake Watters on 5/31/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKManagedObjectMapping.h" -#import "RKHuman.h" -#import "RKMappableObject.h" - -@interface RKManagedObjectMappingSpec : RKSpec { - NSAutoreleasePool *_autoreleasePool; -} - -@end - - -@implementation RKManagedObjectMappingSpec - -//- (void)setUp { -// _autoreleasePool = [NSAutoreleasePool new]; -//} -// -//- (void)tearDown { -// [_autoreleasePool drain]; -//} - -- (void)testShouldReturnTheDefaultValueForACoreDataAttribute { - // Load Core Data - RKSpecNewManagedObjectStore(); - - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKCat"]; - id value = [mapping defaultValueForMissingAttribute:@"name"]; - assertThat(value, is(equalTo(@"Kitty Cat!"))); -} - -- (void)testShouldCreateNewInstancesOfUnmanagedObjects { - RKSpecNewManagedObjectStore(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKMappableObject class]]; - id object = [mapping mappableObjectForData:[NSDictionary dictionary]]; - assertThat(object, isNot(nilValue())); - assertThat([object class], is(equalTo([RKMappableObject class]))); -} - -- (void)testShouldCreateNewInstancesOfManagedObjectsWhenTheMappingIsAnRKObjectMapping { - RKSpecNewManagedObjectStore(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKMappableObject class]]; - id object = [mapping mappableObjectForData:[NSDictionary dictionary]]; - assertThat(object, isNot(nilValue())); - assertThat([object class], is(equalTo([RKMappableObject class]))); -} - -- (void)testShouldFindExistingManagedObjectsByPrimaryKey { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - mapping.primaryKeyAttribute = @"railsID"; - [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - - RKHuman* human = [RKHuman object]; - human.railsID = [NSNumber numberWithInt:123]; - [store save]; - assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); - - NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; - id object = [mapping mappableObjectForData:data]; - assertThat(object, isNot(nilValue())); - assertThat(object, is(equalTo(human))); -} - -- (void)testShouldFindExistingManagedObjectsByPrimaryKeyPath { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); - [RKHuman truncateAll]; - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - mapping.primaryKeyAttribute = @"railsID"; - [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"monkey.id" toKeyPath:@"railsID"]]; - - [RKHuman truncateAll]; - RKHuman* human = [RKHuman object]; - human.railsID = [NSNumber numberWithInt:123]; - [store save]; - 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]; - assertThat(object, isNot(nilValue())); - assertThat(object, is(equalTo(human))); -} - -- (void)testShouldCreateNewManagedObjectInstancesWhenThereIsNoPrimaryKeyInTheData { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - mapping.primaryKeyAttribute = @"railsID"; - - NSDictionary* data = [NSDictionary dictionary]; - id object = [mapping mappableObjectForData:data]; - assertThat(object, isNot(nilValue())); - assertThat(object, is(instanceOf([RKHuman class]))); -} - -- (void)testShouldCreateNewManagedObjectInstancesWhenThereIsNoPrimaryKeyAttribute { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - - NSDictionary* data = [NSDictionary dictionary]; - id object = [mapping mappableObjectForData:data]; - assertThat(object, isNot(nilValue())); - assertThat(object, is(instanceOf([RKHuman class]))); -} - -- (void)testShouldCreateANewManagedObjectWhenThePrimaryKeyValueIsNSNull { - RKSpecNewManagedObjectStore(); - RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - mapping.primaryKeyAttribute = @"railsID"; - [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - - NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNull null] forKey:@"id"]; - id object = [mapping mappableObjectForData:data]; - assertThat(object, isNot(nilValue())); - assertThat(object, is(instanceOf([RKHuman class]))); -} - -- (void)testShouldMapACollectionOfObjectsWithDynamicKeys { - RKManagedObjectStore *objectStore = RKSpecNewManagedObjectStore(); - RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - mapping.forceCollectionMapping = YES; - mapping.primaryKeyAttribute = @"name"; - [mapping mapKeyOfNestedDictionaryToAttribute:@"name"]; - RKObjectAttributeMapping *idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"(name).id" toKeyPath:@"railsID"]; - [mapping addAttributeMapping:idMapping]; - RKObjectMappingProvider *provider = [[RKObjectMappingProvider new] autorelease]; - [provider setMapping:mapping forKeyPath:@"users"]; - - id mockObjectStore = [OCMockObject partialMockForObject:objectStore]; - [[[mockObjectStore expect] andForwardToRealObject] findOrCreateInstanceOfEntity:OCMOCK_ANY withPrimaryKeyAttribute:@"name" andValue:@"blake"]; - [[[mockObjectStore expect] andForwardToRealObject] findOrCreateInstanceOfEntity:mapping.entity withPrimaryKeyAttribute:@"name" andValue:@"rachit"]; - id userInfo = RKSpecParseFixture(@"DynamicKeys.json"); - RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; - [mapper performMapping]; - [mockObjectStore verify]; -} - -@end diff --git a/Specs/CoreData/RKManagedObjectStoreSpec.m b/Specs/CoreData/RKManagedObjectStoreSpec.m deleted file mode 100644 index 31730a4215..0000000000 --- a/Specs/CoreData/RKManagedObjectStoreSpec.m +++ /dev/null @@ -1,46 +0,0 @@ -// -// RKManagedObjectStoreSpec.m -// RestKit -// -// Created by Blake Watters on 7/2/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKHuman.h" - -@interface RKManagedObjectStoreSpec : RKSpec - -@end - -@implementation RKManagedObjectStoreSpec - -- (void)testShouldCoercePrimaryKeysToStringsForLookup { - RKManagedObjectStore* objectStore = RKSpecNewManagedObjectStore(); - RKHuman* human = [RKHuman createEntity]; - human.railsID = [NSNumber numberWithInt:1234]; - [objectStore save]; - NSManagedObject* newReference = [objectStore findOrCreateInstanceOfEntity:[RKHuman entity] withPrimaryKeyAttribute:@"railsID" andValue:@"1234"]; - assertThat(newReference, is(equalTo(human))); -} - -- (void)testShouldStoreNewInstancesOfCreatedObjectsByStringKey { - RKManagedObjectStore* objectStore = RKSpecNewManagedObjectStore(); - NSManagedObject* firstInstance = [objectStore findOrCreateInstanceOfEntity:[RKHuman entity] withPrimaryKeyAttribute:@"railsID" andValue:[NSNumber numberWithInt:1234]]; - NSManagedObject* secondInstance = [objectStore findOrCreateInstanceOfEntity:[RKHuman entity] withPrimaryKeyAttribute:@"railsID" andValue:[NSNumber numberWithInt:1234]]; - assertThat(secondInstance, is(equalTo(firstInstance))); -} - -@end diff --git a/Specs/Fixtures/JSON/Dynamic/boy.json b/Specs/Fixtures/JSON/Dynamic/boy.json deleted file mode 100644 index 9cac23c961..0000000000 --- a/Specs/Fixtures/JSON/Dynamic/boy.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "Blake Watters", "type": "Boy", "numeric_type": 1 } \ No newline at end of file diff --git a/Specs/Fixtures/JSON/Dynamic/girl.json b/Specs/Fixtures/JSON/Dynamic/girl.json deleted file mode 100644 index b4d2ab3d87..0000000000 --- a/Specs/Fixtures/JSON/Dynamic/girl.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "Sarah", "type": "Girl", "numeric_type": 0 } \ No newline at end of file diff --git a/Specs/Fixtures/JSON/error.json b/Specs/Fixtures/JSON/error.json deleted file mode 100644 index e5ddd95ffa..0000000000 --- a/Specs/Fixtures/JSON/error.json +++ /dev/null @@ -1 +0,0 @@ -{error: "this is an error"} \ No newline at end of file diff --git a/Specs/Fixtures/JSON/errors.json b/Specs/Fixtures/JSON/errors.json deleted file mode 100644 index 19dc24bf71..0000000000 --- a/Specs/Fixtures/JSON/errors.json +++ /dev/null @@ -1 +0,0 @@ -{ "errors" : ["error1", "error2"] } \ No newline at end of file diff --git a/Specs/Fixtures/Uploads/.gitignore b/Specs/Fixtures/Uploads/.gitignore deleted file mode 100644 index f59ec20aab..0000000000 --- a/Specs/Fixtures/Uploads/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/Specs/Models/Data Model.xcdatamodel/elements b/Specs/Models/Data Model.xcdatamodel/elements deleted file mode 100644 index daf75f7ebf..0000000000 Binary files a/Specs/Models/Data Model.xcdatamodel/elements and /dev/null differ diff --git a/Specs/Models/Data Model.xcdatamodel/layout b/Specs/Models/Data Model.xcdatamodel/layout deleted file mode 100644 index 472183e651..0000000000 Binary files a/Specs/Models/Data Model.xcdatamodel/layout and /dev/null differ diff --git a/Specs/Network/RKClientSpec.m b/Specs/Network/RKClientSpec.m deleted file mode 100644 index da58066fa7..0000000000 --- a/Specs/Network/RKClientSpec.m +++ /dev/null @@ -1,133 +0,0 @@ -// -// RKClientSpec.m -// RestKit -// -// Created by Blake Watters on 1/31/11. -// Copyright 2011 Two Toasters -// -// 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 -#import "RKSpecEnvironment.h" -#import "RKURL.h" - -@interface RKClientSpec : RKSpec { -} - -@end - - -@implementation RKClientSpec - -- (void)testShouldDetectNetworkStatusWithAHostname { - RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle - RKReachabilityNetworkStatus status = [client.reachabilityObserver networkStatus]; - assertThatInt(status, is(equalToInt(RKReachabilityReachableViaWiFi))); -} - -- (void)testShouldDetectNetworkStatusWithAnIPAddressBaseName { - RKClient* client = [RKClient clientWithBaseURL:@"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))); -} -- (void)testShouldSetTheCachePolicyOfTheRequest { - RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"]; - client.cachePolicy = RKRequestCachePolicyLoadIfOffline; - RKRequest* request = [client requestWithResourcePath:@"" delegate:nil]; - assertThatInt(request.cachePolicy, is(equalToInt(RKRequestCachePolicyLoadIfOffline))); -} - -- (void)testShouldInitializeTheCacheOfTheRequest { - RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"]; - client.requestCache = [[[RKRequestCache alloc] init] autorelease]; - RKRequest* request = [client requestWithResourcePath:@"" delegate:nil]; - assertThat(request.cache, is(equalTo(client.requestCache))); -} - -- (void)testShouldAllowYouToChangeTheBaseURL { - RKClient* client = [RKClient clientWithBaseURL:@"http://www.google.com"]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle - assertThatBool([client isNetworkReachable], is(equalToBool(YES))); - client.baseURL = @"http://www.restkit.org"; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle - assertThatBool([client isNetworkReachable], is(equalToBool(YES))); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKRequest* request = [client requestWithResourcePath:@"/" delegate:loader]; - [request send]; - [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(YES))); -} - -- (void)testShouldLetYouChangeTheHTTPAuthCredentials { - RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); - RKClient *client = RKSpecNewClient(); - client.authenticationType = RKRequestAuthenticationTypeHTTP; - client.username = @"invalid"; - client.password = @"password"; - RKSpecResponseLoader *responseLoader = [RKSpecResponseLoader responseLoader]; - [client get:@"/authentication/basic" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(NO))); - assertThat(responseLoader.failureError, is(notNilValue())); - client.username = @"restkit"; - client.password = @"authentication"; - [client get:@"/authentication/basic" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(YES))); - RKLogConfigureByName("RestKit/Network", RKLogLevelInfo); -} - -- (void)testShouldSuspendTheQueueOnBaseURLChangeWhenReachabilityHasNotBeenEstablished { - RKClient* client = [RKClient clientWithBaseURL:@"http://www.google.com"]; - client.baseURL = @"http://restkit.org"; - assertThatBool(client.requestQueue.suspended, is(equalToBool(YES))); -} - -- (void)testShouldNotSuspendTheMainQueueOnBaseURLChangeWhenReachabilityHasBeenEstablished { - RKReachabilityObserver *observer = [RKReachabilityObserver reachabilityObserverForInternet]; - [observer getFlags]; - assertThatBool([observer isReachabilityDetermined], is(equalToBool(YES))); - RKClient *client = [RKClient clientWithBaseURL:@"http://www.google.com"]; - assertThatBool(client.requestQueue.suspended, is(equalToBool(YES))); - client.reachabilityObserver = observer; - assertThatBool(client.requestQueue.suspended, is(equalToBool(NO))); -} - -- (void)testShouldAllowYouToChangeTheTimeoutInterval { - RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"]; - client.timeoutInterval = 20.0; - RKRequest* request = [client requestWithResourcePath:@"" delegate:nil]; - assertThatFloat(request.timeoutInterval, is(equalToFloat(20.0))); -} - -- (void)testShouldPerformAPUTWithParams { - NSLog(@"PENDING ---> FIX ME!!!"); - return; - RKClient* client = [RKClient clientWithBaseURL:@"http://ohblockhero.appspot.com/api/v1"]; - client.cachePolicy = RKRequestCachePolicyNone; - RKParams *params=[RKParams params]; - [params setValue:@"username" forParam:@"username"]; - [params setValue:@"Dear Daniel" forParam:@"fullName"]; - [params setValue:@"aa@aa.com" forParam:@"email"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; -// loader.timeout = 15; - [client put:@"/userprofile" params:params delegate:loader]; - STAssertNoThrow([loader waitForResponse], @""); - [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(NO))); -} - -@end diff --git a/Specs/Network/RKOAuthClientSpec.m b/Specs/Network/RKOAuthClientSpec.m deleted file mode 100644 index 0461ee9e2a..0000000000 --- a/Specs/Network/RKOAuthClientSpec.m +++ /dev/null @@ -1,68 +0,0 @@ -// -// RKOAuthClientSpec.m -// RestKit -// -// Created by Rodrigo Garcia on 8/4/11. -// Copyright 2011 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 "RKSpecEnvironment.h" - -@interface RKOAuthClientSpec : RKSpec - -@end - -@implementation RKOAuthClientSpec - -- (void)testShouldGetAccessToken{ - RKSpecResponseLoader *loader = [RKSpecResponseLoader responseLoader]; - RKOAuthClient *client = RKSpecNewOAuthClient(loader); - client.authorizationCode = @"1234"; - client.callbackURL = @"http://someURL.com"; - [client validateAuthorizationCode]; - [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(YES))); -} - -- (void)testShouldNotGetAccessToken{ - RKSpecResponseLoader *loader = [RKSpecResponseLoader responseLoader]; - RKOAuthClient *client = RKSpecNewOAuthClient(loader); - client.authorizationCode = @"someInvalidAuthorizationCode"; - client.callbackURL = @"http://someURL.com"; - [client validateAuthorizationCode]; - [loader waitForResponse]; - - assertThatBool(loader.success, is(equalToBool(NO))); - -} -- (void)testShouldGetProtectedResource{ - //TODO: Encapsulate this code in a correct manner - RKSpecResponseLoader *loader = [RKSpecResponseLoader responseLoader]; - RKOAuthClient *client = RKSpecNewOAuthClient(loader); - client.authorizationCode = @"1234"; - client.callbackURL = @"http://someURL.com"; - [client validateAuthorizationCode]; - - RKSpecResponseLoader* resourceLoader = [RKSpecResponseLoader responseLoader]; - RKClient *requestClient = [RKClient clientWithBaseURL:[client authorizationURL]]; - requestClient.OAuth2AccessToken = client.accessToken; - requestClient.authenticationType = RKRequestAuthenticationTypeOAuth2; - RKRequest *request = [requestClient requestWithResourcePath:@"/me" delegate:resourceLoader]; - [request send]; - [resourceLoader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(YES))); -} - -@end diff --git a/Specs/Network/RKResponseSpec.m b/Specs/Network/RKResponseSpec.m deleted file mode 100644 index b62b058c02..0000000000 --- a/Specs/Network/RKResponseSpec.m +++ /dev/null @@ -1,271 +0,0 @@ -// -// RKResponseSpec.m -// RestKit -// -// Created by Blake Watters on 1/15/10. -// Copyright 2010 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKResponse.h" - -@interface RKResponseSpec : RKSpec { - RKResponse* _response; -} - -@end - -@implementation RKResponseSpec - -- (void)setUp { - _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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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))); -} - -- (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"))); -} - -// 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"))); -} - -- (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"))); -} - -- (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))); -} - -- (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))); -} - -- (void)testShouldReturnParseErrorsWhenParsedBodyFails { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - 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]; - assertThat(object, is(nilValue())); - assertThat(error, isNot(nilValue())); - assertThat([error localizedDescription], is(equalTo(@"Unexpected token, wanted '{', '}', '[', ']', ',', ':', 'true', 'false', 'null', '\"STRING\"', 'NUMBER'."))); -} - -- (void)testShouldNotCrashOnFailureToParseBody { - RKResponse *response = [[RKResponse new] autorelease]; - id mockResponse = [OCMockObject partialMockForObject:response]; - [[[mockResponse stub] andReturn:@"test/fake"] MIMEType]; - [[[mockResponse stub] andReturn:@"whatever"] bodyAsString]; - NSError *error = nil; - id parsedResponse = [mockResponse parsedBody:&error]; - assertThat(parsedResponse, is(nilValue())); -} - -- (void)testShouldNotCrashWhenParserReturnsNilWithoutAnError { - RKResponse* response = [[[RKResponse alloc] init] autorelease]; - 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]]; - [[[mockRegistry expect] andReturn:mockParser] parserForMIMEType:RKMIMETypeJSON]; - NSError* error = nil; - [[[mockParser expect] andReturn:nil] objectFromString:@"" error:[OCMArg setTo:error]]; - id object = [mockResponse parsedBody:&error]; - [mockRegistry verify]; - [mockParser verify]; - [RKParserRegistry setSharedRegistry:nil]; - assertThat(object, is(nilValue())); - assertThat(error, is(nilValue())); -} - -- (void)testLoadingNonUTF8Charset { - RKClient* client = RKSpecNewClient(); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - [client get:@"/encoding" delegate:loader]; - [loader waitForResponse]; - assertThat([loader.response bodyEncodingName], is(equalTo(@"us-ascii"))); - assertThatInteger([loader.response bodyEncoding], is(equalToInteger(NSASCIIStringEncoding))); -} - -@end diff --git a/Specs/Network/RKURLSpec.m b/Specs/Network/RKURLSpec.m deleted file mode 100644 index 83f6301518..0000000000 --- a/Specs/Network/RKURLSpec.m +++ /dev/null @@ -1,90 +0,0 @@ -// -// RKURLSpec.m -// RestKit -// -// Created by Blake Watters on 6/29/11. -// Copyright 2011 RestKit -// -// 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 "RKSpecEnvironment.h" -#import "RKURL.h" -#import "NSURL+RestKit.h" - -@interface RKURLSpec : RKSpec -@end - -@implementation RKURLSpec - -- (void)testShouldNotExplodeBecauseOfUnicodeCharacters { - NSException* failed = nil; - @try { - [RKURL URLWithBaseURLString:@"http://test.com" resourcePath:@"/places.json?category=Automóviles"]; - } - @catch (NSException *exception) { - failed = exception; - } - @finally { - NSAssert((failed == nil), @"No exception should be generated by creating URL with Unicode chars"); - } -} - -- (void)testShouldEscapeQueryParameters { - NSDictionary* queryParams = [NSDictionary dictionaryWithObjectsAndKeys:@"What is your #1 e-mail?", @"question", @"john+restkit@gmail.com", @"answer", nil]; - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParams:queryParams]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test?answer=john%2Brestkit%40gmail.com&question=What%20is%20your%20%231%20e-mail%3F"))); -} - -- (void)testShouldHandleNilQueryParameters { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParams:nil]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); -} - -- (void)testShouldHandleEmptyQueryParameters { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParams:[NSDictionary dictionary]]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); -} - -- (void)testShouldHandleResourcePathWithoutLeadingSlash { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"test"]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); -} - -- (void)testShouldHandleEmptyResourcePath { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@""]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org"))); -} - -- (void)testShouldHandleBaseURLsWithAPath { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org/this" resourcePath:@"/test" queryParams:nil]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/this/test"))); -} - -- (void)testShouldSimplifyURLsWithSeveralSlashes { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org//this//" resourcePath:@"/test" queryParams:nil]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/this/test"))); -} - -- (void)testShouldPreserveTrailingSlash { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test/" queryParams:nil]; - assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test/"))); -} - -- (void)testShouldReturnTheMIMETypeForURL { - NSURL *URL = [NSURL URLWithString:@"http://restkit.org/path/to/resource.xml"]; - assertThat([URL MIMETypeForPathExtension], is(equalTo(@"application/xml"))); -} - -@end - \ No newline at end of file diff --git a/Specs/ObjectMapping/RKObjectLoaderSpec.m b/Specs/ObjectMapping/RKObjectLoaderSpec.m deleted file mode 100644 index a89b0c2952..0000000000 --- a/Specs/ObjectMapping/RKObjectLoaderSpec.m +++ /dev/null @@ -1,523 +0,0 @@ -// -// RKObjectLoaderSpec.m -// RestKit -// -// Created by Blake Watters on 4/27/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKObjectMappingProvider.h" -#import "RKErrorMessage.h" -#import "RKJSONParserJSONKit.h" - -// Models -#import "RKObjectLoaderSpecResultModel.h" - -@interface RKSpecComplexUser : NSObject { - NSNumber* _userID; - NSString* _firstname; - NSString* _lastname; - NSString* _email; - NSString* _phone; -} - -@property (nonatomic, retain) NSNumber* userID; -@property (nonatomic, retain) NSString* firstname; -@property (nonatomic, retain) NSString* lastname; -@property (nonatomic, retain) NSString* email; -@property (nonatomic, retain) NSString* phone; - -@end - -@implementation RKSpecComplexUser - -@synthesize userID = _userID; -@synthesize firstname = _firstname; -@synthesize lastname = _lastname; -@synthesize phone = _phone; -@synthesize email = _email; - -+ (NSDictionary*)elementToPropertyMappings { - return [NSDictionary dictionaryWithKeysAndObjects: - @"id", @"userID", - @"firstname", @"firstname", - @"lastname", @"lastname", - @"email", @"email", - @"phone", @"phone", - nil]; -} - -- (void)willSendWithObjectLoader:(RKObjectLoader *)objectLoader { - NSLog(@"RKSpecComplexUser willSendWithObjectLoader: INVOKED!!"); - return; -} - -@end - -@interface RKSpecResponseLoaderWithWillMapData : RKSpecResponseLoader { - id _mappableData; -} - -@property (nonatomic, readonly) id mappableData; - -@end - -@implementation RKSpecResponseLoaderWithWillMapData - -@synthesize mappableData = _mappableData; - -- (void)dealloc { - [_mappableData release]; - [super dealloc]; -} - -- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData { - [*mappableData setValue:@"monkey!" forKey:@"newKey"]; - _mappableData = [*mappableData retain]; -} - -@end - -///////////////////////////////////////////////////////////////////////////// - -@interface RKObjectLoaderSpec : RKSpec { - -} - -@end - -@implementation RKObjectLoaderSpec - -- (RKObjectMappingProvider*)providerForComplexUser { - RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [userMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"firstname" toKeyPath:@"firstname"]]; - [provider setMapping:userMapping forKeyPath:@"data.STUser"]; - return provider; -} - -- (RKObjectMappingProvider*)errorMappingProvider { - RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - RKObjectMapping* errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; - [errorMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"" toKeyPath:@"errorMessage"]]; - [provider setMapping:errorMapping forKeyPath:@"error"]; - [provider setMapping:errorMapping forKeyPath:@"errors"]; - return provider; -} - -- (void)testShouldHandleTheErrorCaseAppropriately { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [objectManager objectLoaderWithResourcePath:@"/errors.json" delegate:responseLoader]; - objectLoader.method = RKRequestMethodGET; - - [objectManager setMappingProvider:[self errorMappingProvider]]; - - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - - assertThat(responseLoader.failureError, isNot(nilValue())); - - assertThat([responseLoader.failureError localizedDescription], is(equalTo(@"error1, error2"))); - - NSArray* objects = [[responseLoader.failureError userInfo] objectForKey:RKObjectMapperErrorObjectsKey]; - RKErrorMessage* error1 = [objects objectAtIndex:0]; - RKErrorMessage* error2 = [objects lastObject]; - - assertThat(error1.errorMessage, is(equalTo(@"error1"))); - assertThat(error2.errorMessage, is(equalTo(@"error2"))); -} - -- (void)testShouldNotCrashWhenLoadingAnErrorResponseWithAnUnmappableMIMEType { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - [objectManager loadObjectsAtResourcePath:@"/404" delegate:loader]; - [loader waitForResponse]; - assertThatBool(loader.unknownResponse, is(equalToBool(YES))); -} - -#pragma mark - Complex JSON - -- (void)testShouldLoadAComplexUserObjectWithTargetObject { - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [objectManager objectLoaderWithResourcePath:@"/JSON/ComplexNestedUser.json" delegate:responseLoader]; - NSString *authString = [NSString stringWithFormat:@"TRUEREST username=%@&password=%@&apikey=123456&class=iphone", @"username", @"password"]; - [objectLoader.URLRequest addValue:authString forHTTPHeaderField:@"Authorization"]; - objectLoader.method = RKRequestMethodGET; - objectLoader.targetObject = user; - - [objectManager setMappingProvider:[self providerForComplexUser]]; - - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - - NSLog(@"Response: %@", responseLoader.objects); - - assertThat(user.firstname, is(equalTo(@"Diego"))); -} - -- (void)testShouldLoadAComplexUserObjectWithoutTargetObject { - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [objectManager objectLoaderWithResourcePath:@"/JSON/ComplexNestedUser.json" delegate:responseLoader]; - objectLoader.method = RKRequestMethodGET; - - [objectManager setMappingProvider:[self providerForComplexUser]]; - - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - assertThatUnsignedInteger([responseLoader.objects count], is(equalToInt(1))); - RKSpecComplexUser* user = [responseLoader.objects lastObject]; - - assertThat(user.firstname, is(equalTo(@"Diego"))); -} - -- (void)testShouldLoadAComplexUserObjectUsingRegisteredKeyPath { - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [objectManager objectLoaderWithResourcePath:@"/JSON/ComplexNestedUser.json" delegate:responseLoader]; - objectLoader.method = RKRequestMethodGET; - - [objectManager setMappingProvider:[self providerForComplexUser]]; - - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - assertThatUnsignedInteger([responseLoader.objects count], is(equalToInt(1))); - RKSpecComplexUser* user = [responseLoader.objects lastObject]; - - assertThat(user.firstname, is(equalTo(@"Diego"))); -} - -#pragma mark - willSendWithObjectLoader: - -- (void)testShouldInvokeWillSendWithObjectLoaderOnSend { -// RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager setMappingProvider:[self providerForComplexUser]]; - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - id mockObject = [OCMockObject partialMockForObject:user]; - - // Explicitly init so we don't get a managed object loader... - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [[RKObjectLoader alloc] initWithResourcePath:@"/" objectManager:objectManager delegate:responseLoader]; - objectLoader.sourceObject = mockObject; - [[mockObject expect] willSendWithObjectLoader:objectLoader]; - [objectLoader send]; - [responseLoader waitForResponse]; - [mockObject verify]; -} - -- (void)testShouldInvokeWillSendWithObjectLoaderOnSendAsynchronously { - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - [objectManager setMappingProvider:[self providerForComplexUser]]; - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - id mockObject = [OCMockObject partialMockForObject:user]; - - // Explicitly init so we don't get a managed object loader... - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [[RKObjectLoader alloc] initWithResourcePath:@"/" objectManager:objectManager delegate:responseLoader]; - objectLoader.sourceObject = mockObject; - [[mockObject expect] willSendWithObjectLoader:objectLoader]; - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - [mockObject verify]; -} - -- (void)testShouldInvokeWillSendWithObjectLoaderOnSendSynchronously { - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - [objectManager setMappingProvider:[self providerForComplexUser]]; - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - id mockObject = [OCMockObject partialMockForObject:user]; - - // Explicitly init so we don't get a managed object loader... - RKObjectLoader* objectLoader = [[RKObjectLoader alloc] initWithResourcePath:@"/" objectManager:objectManager delegate:nil]; - objectLoader.sourceObject = mockObject; - [[mockObject expect] willSendWithObjectLoader:objectLoader]; - [objectLoader sendSynchronously]; - [mockObject verify]; -} - -- (void)testShouldLoadResultsNestedAtAKeyPath { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKObjectMapping* objectMapping = [RKObjectMapping mappingForClass:[RKObjectLoaderSpecResultModel class]]; - [objectMapping mapKeyPath:@"id" toAttribute:@"ID"]; - [objectMapping mapKeyPath:@"ends_at" toAttribute:@"endsAt"]; - [objectMapping mapKeyPath:@"photo_url" toAttribute:@"photoURL"]; - [objectManager.mappingProvider setMapping:objectMapping forKeyPath:@"results"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - [objectManager loadObjectsAtResourcePath:@"/JSON/ArrayOfResults.json" delegate:loader]; - [loader waitForResponse]; - assertThat([loader objects], hasCountOf(2)); - assertThat([[[loader objects] objectAtIndex:0] ID], is(equalToInt(226))); - assertThat([[[loader objects] objectAtIndex:0] photoURL], is(equalTo(@"1308262872.jpg"))); - assertThat([[[loader objects] objectAtIndex:1] ID], is(equalToInt(235))); - assertThat([[[loader objects] objectAtIndex:1] photoURL], is(equalTo(@"1308634984.jpg"))); -} - -- (void)testShouldAllowMutationOfTheParsedDataInWillMapData { - RKSpecResponseLoaderWithWillMapData* loader = (RKSpecResponseLoaderWithWillMapData*)[RKSpecResponseLoaderWithWillMapData responseLoader]; - RKObjectManager* manager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - [manager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader]; - [loader waitForResponse]; - assertThat([loader.mappableData valueForKey:@"newKey"], is(equalTo(@"monkey!"))); -} - -- (void)testShouldAllowYouToPostAnObjectAndHandleAnEmpty204Response { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; - RKObjectMapping* serializationMapping = [mapping inverseMapping]; - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKSpecComplexUser class] toResourcePath:@"/204"]; - [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKSpecComplexUser class]]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* loader = [objectManager objectLoaderForObject:user method:RKRequestMethodPOST delegate:responseLoader]; - loader.objectMapping = mapping; - [loader send]; - [responseLoader waitForResponse]; - assertThatBool([responseLoader success], is(equalToBool(YES))); - assertThat(user.email, is(equalTo(@"blake@restkit.org"))); -} - -- (void)testShouldAllowYouToPOSTAnObjectAndMapBackNonNestedContent { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; - RKObjectMapping* serializationMapping = [mapping inverseMapping]; - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKSpecComplexUser class] toResourcePath:@"/notNestedUser"]; - [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKSpecComplexUser class]]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* loader = [objectManager objectLoaderForObject:user method:RKRequestMethodPOST delegate:responseLoader]; - loader.objectMapping = mapping; - [loader send]; - [responseLoader waitForResponse]; - assertThatBool([responseLoader success], is(equalToBool(YES))); - assertThat(user.email, is(equalTo(@"changed"))); -} - -- (void)testShouldMapContentWithoutAMIMEType { - // TODO: Not sure that this is even worth it. Unable to get the Sinatra server to produce such a response - return; - RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; - RKObjectMapping* serializationMapping = [mapping inverseMapping]; - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [[RKParserRegistry sharedRegistry] setParserClass:[RKJSONParserJSONKit class] forMIMEType:@"text/html"]; - [objectManager.router routeClass:[RKSpecComplexUser class] toResourcePath:@"/noMIME"]; - [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKSpecComplexUser class]]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* loader = [objectManager objectLoaderForObject:user method:RKRequestMethodPOST delegate:responseLoader]; - loader.objectMapping = mapping; - [loader send]; - [responseLoader waitForResponse]; - assertThatBool([responseLoader success], is(equalToBool(YES))); - assertThat(user.email, is(equalTo(@"changed"))); -} - -- (void)testShouldAllowYouToPOSTAnObjectOfOneTypeAndGetBackAnother { - RKObjectMapping* sourceMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [sourceMapping mapAttributes:@"firstname", @"lastname", @"email", nil]; - RKObjectMapping* serializationMapping = [sourceMapping inverseMapping]; - - RKObjectMapping* targetMapping = [RKObjectMapping mappingForClass:[RKObjectLoaderSpecResultModel class]]; - [targetMapping mapAttributes:@"ID", nil]; - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKSpecComplexUser class] toResourcePath:@"/notNestedUser"]; - [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKSpecComplexUser class]]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* loader = [objectManager objectLoaderForObject:user method:RKRequestMethodPOST delegate:responseLoader]; - loader.sourceObject = user; - loader.targetObject = nil; - loader.objectMapping = targetMapping; - [loader send]; - [responseLoader waitForResponse]; - assertThatBool([responseLoader success], is(equalToBool(YES))); - - // Our original object should not have changed - assertThat(user.email, is(equalTo(@"blake@restkit.org"))); - - // And we should have a new one - RKObjectLoaderSpecResultModel* newObject = [[responseLoader objects] lastObject]; - assertThat(newObject, is(instanceOf([RKObjectLoaderSpecResultModel class]))); - assertThat(newObject.ID, is(equalToInt(31337))); -} - -// TODO: Should live in a different file... -- (void)testShouldAllowYouToPOSTAnObjectAndMapBackNonNestedContentViapostObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; - RKObjectMapping* serializationMapping = [mapping inverseMapping]; - - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKSpecComplexUser class] toResourcePath:@"/notNestedUser"]; - [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKSpecComplexUser class]]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - // NOTE: The postObject: should infer the target object from sourceObject and the mapping class - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - [objectManager postObject:user mapResponseWith:mapping delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThatBool([responseLoader success], is(equalToBool(YES))); - assertThat(user.email, is(equalTo(@"changed"))); -} - -- (void)testShouldRespectTheRootKeyPathWhenConstructingATemporaryObjectMappingProvider { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - userMapping.rootKeyPath = @"data.STUser"; - [userMapping mapAttributes:@"firstname", nil]; - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [objectManager objectLoaderWithResourcePath:@"/JSON/ComplexNestedUser.json" delegate:responseLoader]; - objectLoader.objectMapping = userMapping; - objectLoader.method = RKRequestMethodGET; - objectLoader.targetObject = user; - - [objectLoader sendAsynchronously]; - [responseLoader waitForResponse]; - - NSLog(@"Response: %@", responseLoader.objects); - - assertThat(user.firstname, is(equalTo(@"Diego"))); -} - -- (void)testShouldReturnSuccessWhenTheStatusCodeIs200AndTheResponseBodyIsEmpty { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - userMapping.rootKeyPath = @"data.STUser"; - [userMapping mapAttributes:@"firstname", nil]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [RKObjectLoader loaderWithResourcePath:@"/humans/1234" objectManager:objectManager delegate:responseLoader]; - objectLoader.method = RKRequestMethodDELETE; - objectLoader.objectMapping = userMapping; - objectLoader.targetObject = user; - [objectLoader send]; - [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(YES))); -} - -- (void)testShouldInvokeTheDelegateWithTheTargetObjectWhenTheStatusCodeIs200AndTheResponseBodyIsEmpty { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - - RKSpecComplexUser* user = [[RKSpecComplexUser new] autorelease]; - user.firstname = @"Blake"; - user.lastname = @"Watters"; - user.email = @"blake@restkit.org"; - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - userMapping.rootKeyPath = @"data.STUser"; - [userMapping mapAttributes:@"firstname", nil]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader* objectLoader = [RKObjectLoader loaderWithResourcePath:@"/humans/1234" objectManager:objectManager delegate:responseLoader]; - objectLoader.method = RKRequestMethodDELETE; - objectLoader.objectMapping = userMapping; - objectLoader.targetObject = user; - [objectLoader send]; - [responseLoader waitForResponse]; - assertThat(responseLoader.objects, hasItem(user)); -} - -- (void)testShouldConsiderTheLoadOfEmptyObjectsWithoutAnyMappableAttributesAsSuccess { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKSpecComplexUser class]]; - [userMapping mapAttributes:@"firstname", nil]; - [objectManager.mappingProvider setMapping:userMapping forKeyPath:@"firstUser"]; - [objectManager.mappingProvider setMapping:userMapping forKeyPath:@"secondUser"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - [objectManager loadObjectsAtResourcePath:@"/users/empty" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(YES))); -} - -- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyArray { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 20; - [objectManager loadObjectsAtResourcePath:@"/empty/array" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThat(responseLoader.objects, isNot(nilValue())); - assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); - assertThat(responseLoader.objects, is(empty())); -} - -- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyDictionary { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 20; - [objectManager loadObjectsAtResourcePath:@"/empty/dictionary" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThat(responseLoader.objects, isNot(nilValue())); - assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); - assertThat(responseLoader.objects, is(empty())); -} - -- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyString { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - responseLoader.timeout = 20; - [objectManager loadObjectsAtResourcePath:@"/empty/string" delegate:responseLoader]; - [responseLoader waitForResponse]; - assertThat(responseLoader.objects, isNot(nilValue())); - assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); - assertThat(responseLoader.objects, is(empty())); -} - -@end diff --git a/Specs/ObjectMapping/RKObjectMappingProviderSpec.m b/Specs/ObjectMapping/RKObjectMappingProviderSpec.m deleted file mode 100644 index bd74b540ae..0000000000 --- a/Specs/ObjectMapping/RKObjectMappingProviderSpec.m +++ /dev/null @@ -1,79 +0,0 @@ -// -// RKObjectMappingProviderSpec.m -// RestKit -// -// Created by Greg Combs on 9/18/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKObjectManager.h" -#import "RKManagedObjectStore.h" -#import "RKSpecResponseLoader.h" -#import "RKManagedObjectMapping.h" -#import "RKObjectMappingProvider.h" -#import "RKHuman.h" -#import "RKCat.h" -#import "RKObjectMapperSpecModel.h" - -@interface RKObjectMappingProviderSpec : RKSpec { - RKObjectManager* _objectManager; -} - -@end - -@implementation RKObjectMappingProviderSpec - -- (void)setUp { - _objectManager = RKSpecNewObjectManager(); - _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKSpecs.sqlite"]; - [RKObjectManager setSharedManager:_objectManager]; - [_objectManager.objectStore deletePersistantStore]; -} - -- (void)testShouldFindAnExistingObjectMappingForAClass { - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; - assertThat(humanMapping, isNot(equalTo(nil))); - [humanMapping mapAttributes:@"name", nil]; - [_objectManager.mappingProvider addObjectMapping:humanMapping]; - NSObject *returnedMapping = [_objectManager.mappingProvider objectMappingForClass:[RKHuman class]]; - assertThat(returnedMapping, isNot(equalTo(nil))); - assertThat(returnedMapping, is(equalTo(humanMapping))); -} - -- (void)testShouldFindAnExistingObjectMappingForAKeyPath { - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - assertThat(catMapping, isNot(equalTo(nil))); - [catMapping mapAttributes:@"name", nil]; - [_objectManager.mappingProvider setMapping:catMapping forKeyPath:@"cat"]; - NSObject *returnedMapping = [_objectManager.mappingProvider mappingForKeyPath:@"cat"]; - assertThat(returnedMapping, isNot(equalTo(nil))); - assertThat(returnedMapping, is(equalTo(catMapping))); -} - -- (void)testShouldAllowYouToRemoveAMappingByKeyPath { - RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider objectMappingProvider]; - RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; - assertThat(catMapping, isNot(equalTo(nil))); - [catMapping mapAttributes:@"name", nil]; - [mappingProvider setMapping:catMapping forKeyPath:@"cat"]; - NSObject *returnedMapping = [mappingProvider mappingForKeyPath:@"cat"]; - assertThat(returnedMapping, isNot(equalTo(nil))); - [mappingProvider removeMappingForKeyPath:@"cat"]; - returnedMapping = [mappingProvider mappingForKeyPath:@"cat"]; - assertThat(returnedMapping, is(nilValue())); -} - -@end diff --git a/Specs/ObjectMapping/RKParserRegistrySpec.m b/Specs/ObjectMapping/RKParserRegistrySpec.m deleted file mode 100644 index 8f64b7f81d..0000000000 --- a/Specs/ObjectMapping/RKParserRegistrySpec.m +++ /dev/null @@ -1,56 +0,0 @@ -// -// RKParserRegistrySpec.m -// RestKit -// -// Created by Blake Watters on 5/18/11. -// Copyright 2011 Two Toasters -// -// 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 "RKSpecEnvironment.h" -#import "RKParserRegistry.h" -#import "RKJSONParserJSONKit.h" -#import "RKXMLParserLibXML.h" - -@interface RKParserRegistrySpec : RKSpec { -} - -@end - -@implementation RKParserRegistrySpec - -- (void)testShouldEnableRegistrationFromMIMETypeToParserClasses { - RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; - [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; - Class parserClass = [registry parserClassForMIMEType:RKMIMETypeJSON]; - assertThat(NSStringFromClass(parserClass), is(equalTo(@"RKJSONParserJSONKit"))); -} - -- (void)testShouldInstantiateParserObjects { - RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; - [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; - id parser = [registry parserForMIMEType:RKMIMETypeJSON]; - assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); -} - -- (void)testShouldAutoconfigureBasedOnReflection { - RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; - [registry autoconfigure]; - id parser = [registry parserForMIMEType:RKMIMETypeJSON]; - assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); - parser = [registry parserForMIMEType:RKMIMETypeXML]; - assertThat(parser, is(instanceOf([RKXMLParserLibXML class]))); -} - -@end diff --git a/Specs/Runner/RKSpecEnvironment.h b/Specs/Runner/RKSpecEnvironment.h deleted file mode 100644 index 151bc27d7a..0000000000 --- a/Specs/Runner/RKSpecEnvironment.h +++ /dev/null @@ -1,65 +0,0 @@ -// -// RKSpecEnvironment.h -// RestKit -// -// Created by Blake Watters on 1/15/10. -// Copyright 2010 RestKit -// -// 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 -#import - -#define HC_SHORTHAND -#import -#import - -#import "RestKit.h" -#import "RKSpecResponseLoader.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))] - -// The Base URL for the Spec server. See Specs/Server/ -NSString* RKSpecGetBaseURL(void); - -// Stub out the return value of the Shared Client instance's isNetworkAvailable method -void RKSpecStubNetworkAvailability(BOOL isNetworkAvailable); - -// Helpers for returning new instances that clear global state -RKClient* RKSpecNewClient(void); -RKObjectManager* RKSpecNewObjectManager(void); -RKOAuthClient* RKSpecNewOAuthClient(RKSpecResponseLoader* loader); -RKManagedObjectStore* RKSpecNewManagedObjectStore(void); -void RKSpecClearCacheDirectory(void); - -// Read the contents of a fixture file from the app bundle -NSString* RKSpecReadFixture(NSString* fileName); -id RKSpecParseFixture(NSString* fileName); - -// Base class for specs. Allows UISpec to run the specs and use of Hamcrest matchers... -@interface RKSpec : SenTestCase -@end - -@interface SenTestCase (MethodSwizzling) -- (void)swizzleMethod:(SEL)aOriginalMethod - inClass:(Class)aOriginalClass - withMethod:(SEL)aNewMethod - fromClass:(Class)aNewClass - executeBlock:(void (^)(void))aBlock; -@end diff --git a/Specs/Runner/RKSpecEnvironment.m b/Specs/Runner/RKSpecEnvironment.m deleted file mode 100644 index 3e7c867448..0000000000 --- a/Specs/Runner/RKSpecEnvironment.m +++ /dev/null @@ -1,158 +0,0 @@ -// -// RKSpecEnvironment.m -// RestKit -// -// Created by Blake Watters on 3/14/11. -// Copyright 2011 RestKit -// -// 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. -// - -#include -#import "RKSpecEnvironment.h" -#import "RKParserRegistry.h" - -NSString* RKSpecMIMETypeForFixture(NSString* fileName); - -NSString* RKSpecGetBaseURL(void) { - char* ipAddress = getenv("RESTKIT_IP_ADDRESS"); - if (NULL == ipAddress) { - ipAddress = "127.0.0.1"; - } - - return [NSString stringWithFormat:@"http://%s:4567", ipAddress]; -} - -void RKSpecStubNetworkAvailability(BOOL isNetworkAvailable) { - RKClient* client = [RKClient sharedClient]; - if (client) { - id mockClient = [OCMockObject partialMockForObject:client]; - [[[mockClient stub] andReturnValue:OCMOCK_VALUE(isNetworkAvailable)] isNetworkAvailable]; - } -} - -RKClient* RKSpecNewClient(void) { - RKClient* client = [RKClient clientWithBaseURL:RKSpecGetBaseURL()]; - [RKClient setSharedClient:client]; - [client release]; - client.requestQueue.suspended = NO; - - return client; -} - -RKOAuthClient* RKSpecNewOAuthClient(RKSpecResponseLoader* loader){ - [loader setTimeout:10]; - RKOAuthClient* client = [RKOAuthClient clientWithClientID:@"appID" secret:@"appSecret" delegate:loader]; - client.authorizationURL = [NSString stringWithFormat:@"%@/oauth/authorize",RKSpecGetBaseURL()]; - return client; -} - - -RKObjectManager* RKSpecNewObjectManager(void) { - [RKObjectMapping setDefaultDateFormatters:nil]; - RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:RKSpecGetBaseURL()]; - [RKObjectManager setSharedManager:objectManager]; - [RKClient setSharedClient:objectManager.client]; - - // Force reachability determination - [objectManager.client.reachabilityObserver getFlags]; - - return objectManager; -} - -// TODO: Store initialization should not be coupled to object manager... -RKManagedObjectStore* RKSpecNewManagedObjectStore(void) { - RKManagedObjectStore* store = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKSpecs.sqlite"]; - RKObjectManager* objectManager = RKSpecNewObjectManager(); - objectManager.objectStore = store; - [objectManager.objectStore deletePersistantStore]; - return store; -} - -void RKSpecClearCacheDirectory(void) { - NSError* error = nil; - NSString* cachePath = [RKDirectory cachesDirectory]; - BOOL success = [[NSFileManager defaultManager] removeItemAtPath:cachePath error:&error]; - if (success) { - RKLogInfo(@"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]); - } - } else { - RKLogError(@"Failed to clear cache path '%@': %@", cachePath, [error localizedDescription]); - } -} - -// Read a fixture from the app bundle -NSString* RKSpecReadFixture(NSString* fileName) { - NSError* error = nil; - NSBundle *bundle = [NSBundle bundleForClass:[RKSpec class]]; - NSString* filePath = [bundle pathForResource:fileName ofType:nil]; - NSString* fixtureData = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error]; - if (fixtureData == nil && error) { - [NSException raise:nil format:@"Failed to read contents of fixture '%@'. Did you add it to the app bundle? Error: %@", fileName, [error localizedDescription]]; - } - return fixtureData; -} - -NSString* RKSpecMIMETypeForFixture(NSString* fileName) { - NSString* extension = [[fileName pathExtension] lowercaseString]; - if ([extension isEqualToString:@"xml"]) { - return RKMIMETypeXML; - } else if ([extension isEqualToString:@"json"]) { - return RKMIMETypeJSON; - } else { - return nil; - } -} - -id RKSpecParseFixture(NSString* fileName) { - NSError* error = nil; - NSString* data = RKSpecReadFixture(fileName); - NSString* MIMEType = RKSpecMIMETypeForFixture(fileName); - id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:MIMEType]; - id object = [parser objectFromString:data error:&error]; - if (object == nil) { - RKLogCritical(@"Failed to parse JSON fixture '%@'. Error: %@", fileName, [error localizedDescription]); - return nil; - } - - return object; -} - -@implementation RKSpec - -//- (void)failWithException:(NSException *) e { -// printf("%s:%i: error: %s\n", -// [[[e userInfo] objectForKey:SenTestFilenameKey] cString], -// [[[e userInfo] objectForKey:SenTestLineNumberKey] intValue], -// [[[e userInfo] objectForKey:SenTestDescriptionKey] cString]); -// [e raise]; -//} - -@end - -@implementation SenTestCase (MethodSwizzling) -- (void)swizzleMethod:(SEL)aOriginalMethod - inClass:(Class)aOriginalClass - withMethod:(SEL)aNewMethod - fromClass:(Class)aNewClass - 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/Specs/Runner/RKSpecResponseLoader.h b/Specs/Runner/RKSpecResponseLoader.h deleted file mode 100644 index 4e281a363f..0000000000 --- a/Specs/Runner/RKSpecResponseLoader.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// RKSpecResponseLoader.h -// RestKit -// -// Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// -// 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 -#import "RKObjectLoader.h" - -@interface RKSpecResponseLoader : NSObject { - BOOL _awaitingResponse; - BOOL _success; - BOOL _wasCancelled; - BOOL _unknownResponse; - RKResponse* _response; - NSArray* _objects; - NSError* _failureError; - NSString* _errorMessage; - NSTimeInterval _timeout; -} - -// The response that was loaded from the web request -@property (nonatomic, readonly) RKResponse* response; - -// The objects that were loaded (if any) -@property (nonatomic, readonly) NSArray* objects; - -// True when the response is success -@property (nonatomic, readonly) BOOL success; - -// YES when the request was cancelled -@property (nonatomic, readonly) BOOL wasCancelled; - -@property (nonatomic, readonly) BOOL unknownResponse; - -// The error that was returned from a failure to connect -@property (nonatomic, readonly) NSError* failureError; - -// The error message returned by the server -@property (nonatomic, readonly) NSString* errorMessage; - -@property (nonatomic, assign) NSTimeInterval timeout; - -// Return a new auto-released loader -+ (RKSpecResponseLoader*)responseLoader; - -// Wait for a response to load -- (void)waitForResponse; - -@end diff --git a/Specs/Runner/RKSpecResponseLoader.m b/Specs/Runner/RKSpecResponseLoader.m deleted file mode 100644 index 06323c50fa..0000000000 --- a/Specs/Runner/RKSpecResponseLoader.m +++ /dev/null @@ -1,135 +0,0 @@ -// -// RKSpecResponseLoader.m -// RestKit -// -// Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// -// 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 "RKSpecResponseLoader.h" - -NSString * const RKSpecResponseLoaderTimeoutException = @"RKSpecResponseLoaderTimeoutException"; - -@implementation RKSpecResponseLoader - -@synthesize response = _response; -@synthesize objects = _objects; -@synthesize failureError = _failureError; -@synthesize errorMessage = _errorMessage; -@synthesize success = _success; -@synthesize timeout = _timeout; -@synthesize wasCancelled = _wasCancelled; -@synthesize unknownResponse = _unknownResponse; - -+ (RKSpecResponseLoader*)responseLoader { - return [[[self alloc] init] autorelease]; -} - -- (id)init { - self = [super init]; - if (self) { - _timeout = 4; - _awaitingResponse = NO; - } - - return self; -} - -- (void)dealloc { - [_response release]; - [_failureError release]; - [_errorMessage release]; - [super dealloc]; -} - -- (void)waitForResponse { - _awaitingResponse = YES; - NSDate* startDate = [NSDate date]; - - while (_awaitingResponse) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - if ([[NSDate date] timeIntervalSinceDate:startDate] > self.timeout) { - [NSException raise:RKSpecResponseLoaderTimeoutException format:@"*** Operation timed out after %f seconds...", self.timeout]; - _awaitingResponse = NO; - } - } -} - -- (void)loadError:(NSError*)error { - NSLog(@"Error: %@", error); - _awaitingResponse = NO; - _success = NO; - _failureError = [error retain]; -} - -- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response { - NSLog(@"Loaded response: %@", response); - _response = [response retain]; - - // If request is an Object Loader, then objectLoader:didLoadObjects: - // will be sent after didLoadResponse: - if (NO == [request isKindOfClass:[RKObjectLoader class]]) { - _awaitingResponse = NO; - _success = YES; - } -} - -- (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error { - // If request is an Object Loader, then objectLoader:didFailWithError: - // will be sent after didFailLoadWithError: - if (NO == [request isKindOfClass:[RKObjectLoader class]]) { - [self loadError:error]; - } -} - -- (void)requestDidCancelLoad:(RKRequest *)request { - _awaitingResponse = NO; - _success = NO; - _wasCancelled = YES; -} - -- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects { - NSLog(@"Response: %@", [objectLoader.response bodyAsString]); - NSLog(@"Loaded objects: %@", objects); - _objects = [objects retain]; - _awaitingResponse = NO; - _success = YES; -} - -- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error; { - [self loadError:error]; -} - -- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader*)objectLoader { - NSLog(@"*** Loaded unexpected response in spec response loader"); - _success = NO; - _awaitingResponse = NO; - _unknownResponse = YES; -} - -#pragma mark - OAuth delegates - -- (void)OAuthClient:(RKOAuthClient *)client didAcquireAccessToken:(NSString *)token { - _awaitingResponse = NO; - _success = YES; -} - - -- (void)OAuthClient:(RKOAuthClient *)client didFailWithInvalidGrantError:(NSError *)error { - _awaitingResponse = NO; - _success = NO; -} - -@end diff --git a/Specs/Runner/set_ip_address.scpt b/Specs/Runner/set_ip_address.scpt deleted file mode 100644 index 8467fdb329..0000000000 Binary files a/Specs/Runner/set_ip_address.scpt and /dev/null differ diff --git a/Specs/Server/lib/restkit/network/oauth2.rb b/Specs/Server/lib/restkit/network/oauth2.rb deleted file mode 100644 index d362602dab..0000000000 --- a/Specs/Server/lib/restkit/network/oauth2.rb +++ /dev/null @@ -1,37 +0,0 @@ -module RestKit - module Network - class OAuth2 < Sinatra::Base - ACCESS_TOKEN = '581b50dca15a9d41eb280d5cbd52c7da4fb564621247848171508dd9d0dfa551a2efe9d06e110e62335abf13b6446a5c49e4bf6007cd90518fbbb0d1535b4dbc' - - 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 - 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 - end - content_type 'application/json' - response - end - - end - end -end \ No newline at end of file diff --git a/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj b/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..afd7c2cb78 --- /dev/null +++ b/Tests/Application/RKApplicationTests.xcodeproj/project.pbxproj @@ -0,0 +1,1118 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 253B486414E3415800B0483F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486314E3415800B0483F /* UIKit.framework */; }; + 253B486614E3415800B0483F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486514E3415800B0483F /* Foundation.framework */; }; + 253B486814E3415800B0483F /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486714E3415800B0483F /* CoreGraphics.framework */; }; + 253B486A14E3415800B0483F /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486914E3415800B0483F /* CoreData.framework */; }; + 253B487014E3415800B0483F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 253B486E14E3415800B0483F /* InfoPlist.strings */; }; + 253B487214E3415800B0483F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 253B487114E3415800B0483F /* main.m */; }; + 253B487614E3415800B0483F /* RKATAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 253B487514E3415800B0483F /* RKATAppDelegate.m */; }; + 253B488114E3415800B0483F /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B488014E3415800B0483F /* SenTestingKit.framework */; }; + 253B488214E3415800B0483F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486314E3415800B0483F /* UIKit.framework */; }; + 253B488314E3415800B0483F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486514E3415800B0483F /* Foundation.framework */; }; + 253B488414E3415800B0483F /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B486914E3415800B0483F /* CoreData.framework */; }; + 253B488C14E3415800B0483F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 253B488A14E3415800B0483F /* InfoPlist.strings */; }; + 253B48BB14E358D100B0483F /* libRestKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B48A314E341C500B0483F /* libRestKit.a */; }; + 253B495514E35E0400B0483F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B495414E35E0400B0483F /* SystemConfiguration.framework */; }; + 253B495714E35E0F00B0483F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B495614E35E0F00B0483F /* CFNetwork.framework */; }; + 253B495914E35E3300B0483F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B495814E35E3300B0483F /* MobileCoreServices.framework */; }; + 253B495B14E35E3A00B0483F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B495A14E35E3A00B0483F /* Security.framework */; }; + 253B495E14E35EA600B0483F /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 253B495C14E35E8E00B0483F /* QuartzCore.framework */; }; + 255207ED14E5A50800EFD81F /* RKATTests.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 255207EB14E5A50800EFD81F /* RKATTests.xcdatamodeld */; }; + 2552080F14E5AE2200EFD81F /* RKCat.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207F114E5AE2200EFD81F /* RKCat.m */; }; + 2552081014E5AE2200EFD81F /* RKChild.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207F314E5AE2200EFD81F /* RKChild.m */; }; + 2552081114E5AE2200EFD81F /* RKDynamicMappingModels.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207F514E5AE2200EFD81F /* RKDynamicMappingModels.m */; }; + 2552081214E5AE2200EFD81F /* RKEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207F714E5AE2200EFD81F /* RKEvent.m */; }; + 2552081314E5AE2200EFD81F /* RKHouse.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207F914E5AE2200EFD81F /* RKHouse.m */; }; + 2552081414E5AE2200EFD81F /* RKHuman.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207FB14E5AE2200EFD81F /* RKHuman.m */; }; + 2552081514E5AE2200EFD81F /* RKMappableAssociation.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207FD14E5AE2200EFD81F /* RKMappableAssociation.m */; }; + 2552081614E5AE2200EFD81F /* RKMappableObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 255207FF14E5AE2200EFD81F /* RKMappableObject.m */; }; + 2552081714E5AE2200EFD81F /* RKObjectLoaderTestResultModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080114E5AE2200EFD81F /* RKObjectLoaderTestResultModel.m */; }; + 2552081814E5AE2200EFD81F /* RKObjectMapperTestModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080314E5AE2200EFD81F /* RKObjectMapperTestModel.m */; }; + 2552081914E5AE2200EFD81F /* RKParent.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080514E5AE2200EFD81F /* RKParent.m */; }; + 2552081A14E5AE2200EFD81F /* RKResident.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080714E5AE2200EFD81F /* RKResident.m */; }; + 2552081B14E5AE2200EFD81F /* RKSearchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080914E5AE2200EFD81F /* RKSearchable.m */; }; + 2552081C14E5AE2200EFD81F /* RKTestAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080B14E5AE2200EFD81F /* RKTestAddress.m */; }; + 2552081D14E5AE2200EFD81F /* RKTestUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552080D14E5AE2200EFD81F /* RKTestUser.m */; }; + 2552085514E5AE4900EFD81F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2552082414E5AE4900EFD81F /* InfoPlist.strings */; }; + 2552085614E5AE4900EFD81F /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552082714E5AE4900EFD81F /* NSInvocation+OCMAdditions.m */; }; + 2552085714E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552082914E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.m */; }; + 2552085814E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552082B14E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.m */; }; + 2552085914E5AE4900EFD81F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552082D14E5AE4900EFD81F /* OCClassMockObject.m */; }; + 2552085A14E5AE4900EFD81F /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552082F14E5AE4900EFD81F /* OCMArg.m */; }; + 2552085B14E5AE4900EFD81F /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083114E5AE4900EFD81F /* OCMBlockCaller.m */; }; + 2552085C14E5AE4900EFD81F /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083314E5AE4900EFD81F /* OCMBoxedReturnValueProvider.m */; }; + 2552085D14E5AE4900EFD81F /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083514E5AE4900EFD81F /* OCMConstraint.m */; }; + 2552085E14E5AE4900EFD81F /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083714E5AE4900EFD81F /* OCMExceptionReturnValueProvider.m */; }; + 2552085F14E5AE4900EFD81F /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083914E5AE4900EFD81F /* OCMIndirectReturnValueProvider.m */; }; + 2552086014E5AE4900EFD81F /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083B14E5AE4900EFD81F /* OCMNotificationPoster.m */; }; + 2552086114E5AE4900EFD81F /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552083D14E5AE4900EFD81F /* OCMObserverRecorder.m */; }; + 2552086214E5AE4900EFD81F /* OCMock-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2552083E14E5AE4900EFD81F /* OCMock-Info.plist */; }; + 2552086314E5AE4900EFD81F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084214E5AE4900EFD81F /* OCMockObject.m */; }; + 2552086414E5AE4900EFD81F /* OCMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084414E5AE4900EFD81F /* OCMockRecorder.m */; }; + 2552086514E5AE4900EFD81F /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084614E5AE4900EFD81F /* OCMPassByRefSetter.m */; }; + 2552086614E5AE4900EFD81F /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084814E5AE4900EFD81F /* OCMRealObjectForwarder.m */; }; + 2552086714E5AE4900EFD81F /* OCMReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084A14E5AE4900EFD81F /* OCMReturnValueProvider.m */; }; + 2552086814E5AE4900EFD81F /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084C14E5AE4900EFD81F /* OCObserverMockObject.m */; }; + 2552086914E5AE4900EFD81F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552084E14E5AE4900EFD81F /* OCPartialMockObject.m */; }; + 2552086A14E5AE4900EFD81F /* OCPartialMockRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552085014E5AE4900EFD81F /* OCPartialMockRecorder.m */; }; + 2552086B14E5AE4900EFD81F /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552085214E5AE4900EFD81F /* OCProtocolMockObject.m */; }; + 2552086C14E5AFDC00EFD81F /* OCHamcrestIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2552082114E5AE4900EFD81F /* OCHamcrestIOS.framework */; }; + 2552087E14E5B0B500EFD81F /* RKTestEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 2552087D14E5B0B500EFD81F /* RKTestEnvironment.m */; }; + 25FABEA914E3698100E609E7 /* blake.png in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8114E3698100E609E7 /* blake.png */; }; + 25FABEAA14E3698100E609E7 /* ArrayOfNestedDictionaries.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8314E3698100E609E7 /* ArrayOfNestedDictionaries.json */; }; + 25FABEAB14E3698100E609E7 /* ArrayOfResults.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8414E3698100E609E7 /* ArrayOfResults.json */; }; + 25FABEAC14E3698100E609E7 /* ComplexNestedUser.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8514E3698100E609E7 /* ComplexNestedUser.json */; }; + 25FABEAD14E3698100E609E7 /* ConnectingParents.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8614E3698100E609E7 /* ConnectingParents.json */; }; + 25FABEAE14E3698100E609E7 /* boy.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8814E3698100E609E7 /* boy.json */; }; + 25FABEAF14E3698100E609E7 /* child.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8914E3698100E609E7 /* child.json */; }; + 25FABEB014E3698100E609E7 /* friends.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8A14E3698100E609E7 /* friends.json */; }; + 25FABEB114E3698100E609E7 /* girl.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8B14E3698100E609E7 /* girl.json */; }; + 25FABEB214E3698100E609E7 /* mixed.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8C14E3698100E609E7 /* mixed.json */; }; + 25FABEB314E3698100E609E7 /* parent.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8D14E3698100E609E7 /* parent.json */; }; + 25FABEB414E3698100E609E7 /* DynamicKeys.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8E14E3698100E609E7 /* DynamicKeys.json */; }; + 25FABEB514E3698100E609E7 /* DynamicKeysWithNestedRelationship.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE8F14E3698100E609E7 /* DynamicKeysWithNestedRelationship.json */; }; + 25FABEB614E3698100E609E7 /* DynamicKeysWithRelationship.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9014E3698100E609E7 /* DynamicKeysWithRelationship.json */; }; + 25FABEB714E3698100E609E7 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9114E3698100E609E7 /* error.json */; }; + 25FABEB814E3698100E609E7 /* errors.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9214E3698100E609E7 /* errors.json */; }; + 25FABEB914E3698100E609E7 /* Foursquare.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9314E3698100E609E7 /* Foursquare.json */; }; + 25FABEBA14E3698100E609E7 /* 1.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9514E3698100E609E7 /* 1.json */; }; + 25FABEBB14E3698100E609E7 /* all.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9614E3698100E609E7 /* all.json */; }; + 25FABEBC14E3698100E609E7 /* with_to_one_relationship.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9714E3698100E609E7 /* with_to_one_relationship.json */; }; + 25FABEBD14E3698100E609E7 /* NakedEvents.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9814E3698100E609E7 /* NakedEvents.json */; }; + 25FABEBE14E3698100E609E7 /* nested_user.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9914E3698100E609E7 /* nested_user.json */; }; + 25FABEBF14E3698100E609E7 /* RailsUser.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9A14E3698100E609E7 /* RailsUser.json */; }; + 25FABEC014E3698100E609E7 /* SameKeyDifferentTargetClasses.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9B14E3698100E609E7 /* SameKeyDifferentTargetClasses.json */; }; + 25FABEC114E3698100E609E7 /* user.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9C14E3698100E609E7 /* user.json */; }; + 25FABEC214E3698100E609E7 /* users.json in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9D14E3698100E609E7 /* users.json */; }; + 25FABEC314E3698100E609E7 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 25FABE9F14E3698100E609E7 /* .gitignore */; }; + 25FABEC414E3698100E609E7 /* file in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA014E3698100E609E7 /* file */; }; + 25FABEC514E3698100E609E7 /* attributes_without_text_content.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA214E3698100E609E7 /* attributes_without_text_content.xml */; }; + 25FABEC614E3698100E609E7 /* channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA314E3698100E609E7 /* channels.xml */; }; + 25FABEC714E3698100E609E7 /* container_attributes.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA414E3698100E609E7 /* container_attributes.xml */; }; + 25FABEC814E3698100E609E7 /* national_weather_service.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA514E3698100E609E7 /* national_weather_service.xml */; }; + 25FABEC914E3698100E609E7 /* orders.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA614E3698100E609E7 /* orders.xml */; }; + 25FABECA14E3698100E609E7 /* tab_data.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA714E3698100E609E7 /* tab_data.xml */; }; + 25FABECB14E3698100E609E7 /* zend.xml in Resources */ = {isa = PBXBuildFile; fileRef = 25FABEA814E3698100E609E7 /* zend.xml */; }; + 25FCB40614E5B44C00A0FF3F /* RKFetchedResultsTableControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB3FF14E5B44C00A0FF3F /* RKFetchedResultsTableControllerTest.m */; }; + 25FCB40814E5B44C00A0FF3F /* RKFormTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB40114E5B44C00A0FF3F /* RKFormTest.m */; }; + 25FCB40914E5B44C00A0FF3F /* RKTableControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB40214E5B44C00A0FF3F /* RKTableControllerTest.m */; }; + 25FCB40A14E5B44C00A0FF3F /* RKTableViewSectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB40314E5B44C00A0FF3F /* RKTableViewSectionTest.m */; }; + 25FCB40B14E5B44C00A0FF3F /* RKTableViewCellMappingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB40414E5B44C00A0FF3F /* RKTableViewCellMappingTest.m */; }; + 25FCB40C14E5B44C00A0FF3F /* RKTableViewCellMappingsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FCB40514E5B44C00A0FF3F /* RKTableViewCellMappingsTest.m */; }; + 25FCB40D14E5B5BC00A0FF3F /* Data Model.xcdatamodel in Sources */ = {isa = PBXBuildFile; fileRef = 255207EF14E5AE2200EFD81F /* Data Model.xcdatamodel */; }; +/* 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 */; + proxyType = 1; + remoteGlobalIDString = 253B485E14E3415800B0483F; + remoteInfo = RKUITests; + }; + 253B48A214E341C500B0483F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 25160D1614564E810060A5C5; + remoteInfo = RestKit; + }; + 253B48A414E341C500B0483F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 25160D2614564E820060A5C5; + remoteInfo = RestKitTests; + }; + 253B48A614E341C500B0483F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 25160E62145651060060A5C5; + remoteInfo = RestKitFramework; + }; + 253B48A814E341C500B0483F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 25160E78145651060060A5C5; + remoteInfo = RestKitFrameworkTests; + }; + 253B48AA14E341DD00B0483F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 25160D1514564E810060A5C5; + remoteInfo = RestKit; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 253B485F14E3415800B0483F /* RKApplicationTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RKApplicationTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 253B486314E3415800B0483F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 253B486514E3415800B0483F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 253B486714E3415800B0483F /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 253B486914E3415800B0483F /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + 253B486D14E3415800B0483F /* RKApplicationTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RKApplicationTests-Info.plist"; sourceTree = ""; }; + 253B486F14E3415800B0483F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 253B487114E3415800B0483F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 253B487314E3415800B0483F /* RKApplicationTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RKApplicationTests-Prefix.pch"; sourceTree = ""; }; + 253B487414E3415800B0483F /* RKATAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RKATAppDelegate.h; sourceTree = ""; }; + 253B487514E3415800B0483F /* RKATAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RKATAppDelegate.m; sourceTree = ""; }; + 253B487F14E3415800B0483F /* Application Tests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Application Tests.octest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 253B488014E3415800B0483F /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 253B488914E3415800B0483F /* RKApplicationTestsTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RKApplicationTestsTests-Info.plist"; sourceTree = ""; }; + 253B488B14E3415800B0483F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 253B489814E341C500B0483F /* RestKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RestKit.xcodeproj; path = ../../RestKit.xcodeproj; sourceTree = ""; }; + 253B495414E35E0400B0483F /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 253B495614E35E0F00B0483F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 253B495814E35E3300B0483F /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 253B495A14E35E3A00B0483F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 253B495C14E35E8E00B0483F /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 255207EC14E5A50800EFD81F /* RKUITests.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RKUITests.xcdatamodel; sourceTree = ""; }; + 255207EF14E5AE2200EFD81F /* Data Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Data Model.xcdatamodel"; sourceTree = ""; }; + 255207F014E5AE2200EFD81F /* RKCat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKCat.h; sourceTree = ""; }; + 255207F114E5AE2200EFD81F /* RKCat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKCat.m; sourceTree = ""; }; + 255207F214E5AE2200EFD81F /* RKChild.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKChild.h; sourceTree = ""; }; + 255207F314E5AE2200EFD81F /* RKChild.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKChild.m; sourceTree = ""; }; + 255207F414E5AE2200EFD81F /* RKDynamicMappingModels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKDynamicMappingModels.h; sourceTree = ""; }; + 255207F514E5AE2200EFD81F /* RKDynamicMappingModels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKDynamicMappingModels.m; sourceTree = ""; }; + 255207F614E5AE2200EFD81F /* RKEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEvent.h; sourceTree = ""; }; + 255207F714E5AE2200EFD81F /* RKEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEvent.m; sourceTree = ""; }; + 255207F814E5AE2200EFD81F /* RKHouse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKHouse.h; sourceTree = ""; }; + 255207F914E5AE2200EFD81F /* RKHouse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKHouse.m; sourceTree = ""; }; + 255207FA14E5AE2200EFD81F /* RKHuman.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKHuman.h; sourceTree = ""; }; + 255207FB14E5AE2200EFD81F /* RKHuman.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKHuman.m; sourceTree = ""; }; + 255207FC14E5AE2200EFD81F /* RKMappableAssociation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKMappableAssociation.h; sourceTree = ""; }; + 255207FD14E5AE2200EFD81F /* RKMappableAssociation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMappableAssociation.m; sourceTree = ""; }; + 255207FE14E5AE2200EFD81F /* RKMappableObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKMappableObject.h; sourceTree = ""; }; + 255207FF14E5AE2200EFD81F /* RKMappableObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKMappableObject.m; sourceTree = ""; }; + 2552080014E5AE2200EFD81F /* RKObjectLoaderTestResultModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectLoaderTestResultModel.h; sourceTree = ""; }; + 2552080114E5AE2200EFD81F /* RKObjectLoaderTestResultModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectLoaderTestResultModel.m; sourceTree = ""; }; + 2552080214E5AE2200EFD81F /* RKObjectMapperTestModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectMapperTestModel.h; sourceTree = ""; }; + 2552080314E5AE2200EFD81F /* RKObjectMapperTestModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMapperTestModel.m; sourceTree = ""; }; + 2552080414E5AE2200EFD81F /* RKParent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKParent.h; sourceTree = ""; }; + 2552080514E5AE2200EFD81F /* RKParent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKParent.m; sourceTree = ""; }; + 2552080614E5AE2200EFD81F /* RKResident.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKResident.h; sourceTree = ""; }; + 2552080714E5AE2200EFD81F /* RKResident.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKResident.m; sourceTree = ""; }; + 2552080814E5AE2200EFD81F /* RKSearchable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchable.h; sourceTree = ""; }; + 2552080914E5AE2200EFD81F /* RKSearchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchable.m; sourceTree = ""; }; + 2552080A14E5AE2200EFD81F /* RKTestAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestAddress.h; sourceTree = ""; }; + 2552080B14E5AE2200EFD81F /* RKTestAddress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestAddress.m; sourceTree = ""; }; + 2552080C14E5AE2200EFD81F /* RKTestUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestUser.h; sourceTree = ""; }; + 2552080D14E5AE2200EFD81F /* RKTestUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestUser.m; sourceTree = ""; }; + 2552082014E5AE4900EFD81F /* OCHamcrest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrest.framework; sourceTree = ""; }; + 2552082114E5AE4900EFD81F /* OCHamcrestIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrestIOS.framework; sourceTree = ""; }; + 2552082514E5AE4900EFD81F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 2552082614E5AE4900EFD81F /* NSInvocation+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+OCMAdditions.h"; sourceTree = ""; }; + 2552082714E5AE4900EFD81F /* NSInvocation+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+OCMAdditions.m"; sourceTree = ""; }; + 2552082814E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+OCMAdditions.h"; sourceTree = ""; }; + 2552082914E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+OCMAdditions.m"; sourceTree = ""; }; + 2552082A14E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNotificationCenter+OCMAdditions.h"; sourceTree = ""; }; + 2552082B14E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OCMAdditions.m"; sourceTree = ""; }; + 2552082C14E5AE4900EFD81F /* OCClassMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCClassMockObject.h; sourceTree = ""; }; + 2552082D14E5AE4900EFD81F /* OCClassMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCClassMockObject.m; sourceTree = ""; }; + 2552082E14E5AE4900EFD81F /* OCMArg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMArg.h; sourceTree = ""; }; + 2552082F14E5AE4900EFD81F /* OCMArg.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArg.m; sourceTree = ""; }; + 2552083014E5AE4900EFD81F /* OCMBlockCaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBlockCaller.h; sourceTree = ""; }; + 2552083114E5AE4900EFD81F /* OCMBlockCaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBlockCaller.m; sourceTree = ""; }; + 2552083214E5AE4900EFD81F /* OCMBoxedReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBoxedReturnValueProvider.h; sourceTree = ""; }; + 2552083314E5AE4900EFD81F /* OCMBoxedReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProvider.m; sourceTree = ""; }; + 2552083414E5AE4900EFD81F /* OCMConstraint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMConstraint.h; sourceTree = ""; }; + 2552083514E5AE4900EFD81F /* OCMConstraint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMConstraint.m; sourceTree = ""; }; + 2552083614E5AE4900EFD81F /* OCMExceptionReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMExceptionReturnValueProvider.h; sourceTree = ""; }; + 2552083714E5AE4900EFD81F /* OCMExceptionReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMExceptionReturnValueProvider.m; sourceTree = ""; }; + 2552083814E5AE4900EFD81F /* OCMIndirectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMIndirectReturnValueProvider.h; sourceTree = ""; }; + 2552083914E5AE4900EFD81F /* OCMIndirectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIndirectReturnValueProvider.m; sourceTree = ""; }; + 2552083A14E5AE4900EFD81F /* OCMNotificationPoster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMNotificationPoster.h; sourceTree = ""; }; + 2552083B14E5AE4900EFD81F /* OCMNotificationPoster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNotificationPoster.m; sourceTree = ""; }; + 2552083C14E5AE4900EFD81F /* OCMObserverRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMObserverRecorder.h; sourceTree = ""; }; + 2552083D14E5AE4900EFD81F /* OCMObserverRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMObserverRecorder.m; sourceTree = ""; }; + 2552083E14E5AE4900EFD81F /* OCMock-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "OCMock-Info.plist"; sourceTree = ""; }; + 2552083F14E5AE4900EFD81F /* OCMock-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMock-Prefix.pch"; sourceTree = ""; }; + 2552084014E5AE4900EFD81F /* OCMock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMock.h; sourceTree = ""; }; + 2552084114E5AE4900EFD81F /* OCMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockObject.h; sourceTree = ""; }; + 2552084214E5AE4900EFD81F /* OCMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObject.m; sourceTree = ""; }; + 2552084314E5AE4900EFD81F /* OCMockRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockRecorder.h; sourceTree = ""; }; + 2552084414E5AE4900EFD81F /* OCMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockRecorder.m; sourceTree = ""; }; + 2552084514E5AE4900EFD81F /* OCMPassByRefSetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMPassByRefSetter.h; sourceTree = ""; }; + 2552084614E5AE4900EFD81F /* OCMPassByRefSetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMPassByRefSetter.m; sourceTree = ""; }; + 2552084714E5AE4900EFD81F /* OCMRealObjectForwarder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRealObjectForwarder.h; sourceTree = ""; }; + 2552084814E5AE4900EFD81F /* OCMRealObjectForwarder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMRealObjectForwarder.m; sourceTree = ""; }; + 2552084914E5AE4900EFD81F /* OCMReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMReturnValueProvider.h; sourceTree = ""; }; + 2552084A14E5AE4900EFD81F /* OCMReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMReturnValueProvider.m; sourceTree = ""; }; + 2552084B14E5AE4900EFD81F /* OCObserverMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCObserverMockObject.h; sourceTree = ""; }; + 2552084C14E5AE4900EFD81F /* OCObserverMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCObserverMockObject.m; sourceTree = ""; }; + 2552084D14E5AE4900EFD81F /* OCPartialMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockObject.h; sourceTree = ""; }; + 2552084E14E5AE4900EFD81F /* OCPartialMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockObject.m; sourceTree = ""; }; + 2552084F14E5AE4900EFD81F /* OCPartialMockRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockRecorder.h; sourceTree = ""; }; + 2552085014E5AE4900EFD81F /* OCPartialMockRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockRecorder.m; sourceTree = ""; }; + 2552085114E5AE4900EFD81F /* OCProtocolMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCProtocolMockObject.h; sourceTree = ""; }; + 2552085214E5AE4900EFD81F /* OCProtocolMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCProtocolMockObject.m; sourceTree = ""; }; + 2552087C14E5B0B500EFD81F /* RKTestEnvironment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKTestEnvironment.h; sourceTree = ""; }; + 2552087D14E5B0B500EFD81F /* RKTestEnvironment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTestEnvironment.m; sourceTree = ""; }; + 25FABE8114E3698100E609E7 /* blake.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blake.png; sourceTree = ""; }; + 25FABE8314E3698100E609E7 /* ArrayOfNestedDictionaries.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfNestedDictionaries.json; sourceTree = ""; }; + 25FABE8414E3698100E609E7 /* ArrayOfResults.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ArrayOfResults.json; sourceTree = ""; }; + 25FABE8514E3698100E609E7 /* ComplexNestedUser.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ComplexNestedUser.json; sourceTree = ""; }; + 25FABE8614E3698100E609E7 /* ConnectingParents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ConnectingParents.json; sourceTree = ""; }; + 25FABE8814E3698100E609E7 /* boy.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = boy.json; sourceTree = ""; }; + 25FABE8914E3698100E609E7 /* child.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = child.json; sourceTree = ""; }; + 25FABE8A14E3698100E609E7 /* friends.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = friends.json; sourceTree = ""; }; + 25FABE8B14E3698100E609E7 /* girl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = girl.json; sourceTree = ""; }; + 25FABE8C14E3698100E609E7 /* mixed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mixed.json; sourceTree = ""; }; + 25FABE8D14E3698100E609E7 /* parent.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = parent.json; sourceTree = ""; }; + 25FABE8E14E3698100E609E7 /* DynamicKeys.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = DynamicKeys.json; sourceTree = ""; }; + 25FABE8F14E3698100E609E7 /* DynamicKeysWithNestedRelationship.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = DynamicKeysWithNestedRelationship.json; sourceTree = ""; }; + 25FABE9014E3698100E609E7 /* DynamicKeysWithRelationship.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = DynamicKeysWithRelationship.json; sourceTree = ""; }; + 25FABE9114E3698100E609E7 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; + 25FABE9214E3698100E609E7 /* errors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = errors.json; sourceTree = ""; }; + 25FABE9314E3698100E609E7 /* Foursquare.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Foursquare.json; sourceTree = ""; }; + 25FABE9514E3698100E609E7 /* 1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 1.json; sourceTree = ""; }; + 25FABE9614E3698100E609E7 /* all.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = all.json; sourceTree = ""; }; + 25FABE9714E3698100E609E7 /* with_to_one_relationship.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = with_to_one_relationship.json; sourceTree = ""; }; + 25FABE9814E3698100E609E7 /* NakedEvents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = NakedEvents.json; sourceTree = ""; }; + 25FABE9914E3698100E609E7 /* nested_user.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = nested_user.json; sourceTree = ""; }; + 25FABE9A14E3698100E609E7 /* RailsUser.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RailsUser.json; sourceTree = ""; }; + 25FABE9B14E3698100E609E7 /* SameKeyDifferentTargetClasses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SameKeyDifferentTargetClasses.json; sourceTree = ""; }; + 25FABE9C14E3698100E609E7 /* user.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = user.json; sourceTree = ""; }; + 25FABE9D14E3698100E609E7 /* users.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = users.json; sourceTree = ""; }; + 25FABE9F14E3698100E609E7 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + 25FABEA014E3698100E609E7 /* file */ = {isa = PBXFileReference; lastKnownFileType = file; path = file; sourceTree = ""; }; + 25FABEA214E3698100E609E7 /* attributes_without_text_content.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = attributes_without_text_content.xml; sourceTree = ""; }; + 25FABEA314E3698100E609E7 /* channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = channels.xml; sourceTree = ""; }; + 25FABEA414E3698100E609E7 /* container_attributes.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = container_attributes.xml; sourceTree = ""; }; + 25FABEA514E3698100E609E7 /* national_weather_service.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = national_weather_service.xml; sourceTree = ""; }; + 25FABEA614E3698100E609E7 /* orders.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = orders.xml; sourceTree = ""; }; + 25FABEA714E3698100E609E7 /* tab_data.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = tab_data.xml; sourceTree = ""; }; + 25FABEA814E3698100E609E7 /* zend.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = zend.xml; sourceTree = ""; }; + 25FCB3FF14E5B44C00A0FF3F /* RKFetchedResultsTableControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFetchedResultsTableControllerTest.m; sourceTree = ""; }; + 25FCB40114E5B44C00A0FF3F /* RKFormTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKFormTest.m; sourceTree = ""; }; + 25FCB40214E5B44C00A0FF3F /* RKTableControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableControllerTest.m; sourceTree = ""; }; + 25FCB40314E5B44C00A0FF3F /* RKTableViewSectionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableViewSectionTest.m; sourceTree = ""; }; + 25FCB40414E5B44C00A0FF3F /* RKTableViewCellMappingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableViewCellMappingTest.m; sourceTree = ""; }; + 25FCB40514E5B44C00A0FF3F /* RKTableViewCellMappingsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKTableViewCellMappingsTest.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 253B485C14E3415800B0483F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 253B495E14E35EA600B0483F /* QuartzCore.framework in Frameworks */, + 253B495B14E35E3A00B0483F /* Security.framework in Frameworks */, + 253B495914E35E3300B0483F /* MobileCoreServices.framework in Frameworks */, + 253B495714E35E0F00B0483F /* CFNetwork.framework in Frameworks */, + 253B495514E35E0400B0483F /* SystemConfiguration.framework in Frameworks */, + 253B48BB14E358D100B0483F /* libRestKit.a in Frameworks */, + 253B486414E3415800B0483F /* UIKit.framework in Frameworks */, + 253B486614E3415800B0483F /* Foundation.framework in Frameworks */, + 253B486814E3415800B0483F /* CoreGraphics.framework in Frameworks */, + 253B486A14E3415800B0483F /* CoreData.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 253B487B14E3415800B0483F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 253B488114E3415800B0483F /* SenTestingKit.framework in Frameworks */, + 253B488214E3415800B0483F /* UIKit.framework in Frameworks */, + 253B488314E3415800B0483F /* Foundation.framework in Frameworks */, + 253B488414E3415800B0483F /* CoreData.framework in Frameworks */, + 2552086C14E5AFDC00EFD81F /* OCHamcrestIOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 253B485414E3415800B0483F = { + isa = PBXGroup; + children = ( + 253B489814E341C500B0483F /* RestKit.xcodeproj */, + 253B486B14E3415800B0483F /* App */, + 253B488714E3415800B0483F /* Tests */, + 253B486214E3415800B0483F /* Frameworks */, + 253B486014E3415800B0483F /* Products */, + ); + sourceTree = ""; + }; + 253B486014E3415800B0483F /* Products */ = { + isa = PBXGroup; + children = ( + 253B485F14E3415800B0483F /* RKApplicationTests.app */, + 253B487F14E3415800B0483F /* Application Tests.octest */, + ); + name = Products; + sourceTree = ""; + }; + 253B486214E3415800B0483F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 253B495C14E35E8E00B0483F /* QuartzCore.framework */, + 253B495A14E35E3A00B0483F /* Security.framework */, + 253B495814E35E3300B0483F /* MobileCoreServices.framework */, + 253B495614E35E0F00B0483F /* CFNetwork.framework */, + 253B495414E35E0400B0483F /* SystemConfiguration.framework */, + 253B486314E3415800B0483F /* UIKit.framework */, + 253B486514E3415800B0483F /* Foundation.framework */, + 253B486714E3415800B0483F /* CoreGraphics.framework */, + 253B486914E3415800B0483F /* CoreData.framework */, + 253B488014E3415800B0483F /* SenTestingKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 253B486B14E3415800B0483F /* App */ = { + isa = PBXGroup; + children = ( + 253B487414E3415800B0483F /* RKATAppDelegate.h */, + 253B487514E3415800B0483F /* RKATAppDelegate.m */, + 255207EB14E5A50800EFD81F /* RKATTests.xcdatamodeld */, + 253B486C14E3415800B0483F /* Supporting Files */, + ); + name = App; + path = RKApplicationTests/App; + sourceTree = ""; + }; + 253B486C14E3415800B0483F /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 253B486D14E3415800B0483F /* RKApplicationTests-Info.plist */, + 253B486E14E3415800B0483F /* InfoPlist.strings */, + 253B487114E3415800B0483F /* main.m */, + 253B487314E3415800B0483F /* RKApplicationTests-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 253B488714E3415800B0483F /* Tests */ = { + isa = PBXGroup; + children = ( + 25FCB3FE14E5B44C00A0FF3F /* UI */, + 2552087C14E5B0B500EFD81F /* RKTestEnvironment.h */, + 2552087D14E5B0B500EFD81F /* RKTestEnvironment.m */, + 2552081E14E5AE4900EFD81F /* Vendor */, + 255207EE14E5AE2200EFD81F /* Models */, + 25FABE7F14E3698100E609E7 /* Fixtures */, + 253B488814E3415800B0483F /* Supporting Files */, + ); + name = Tests; + path = ..; + sourceTree = ""; + }; + 253B488814E3415800B0483F /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 253B488914E3415800B0483F /* RKApplicationTestsTests-Info.plist */, + 253B488A14E3415800B0483F /* InfoPlist.strings */, + ); + name = "Supporting Files"; + path = Application/RKApplicationTests/Tests; + sourceTree = ""; + }; + 253B489914E341C500B0483F /* Products */ = { + isa = PBXGroup; + children = ( + 253B48A314E341C500B0483F /* libRestKit.a */, + 253B48A514E341C500B0483F /* RestKitTests.octest */, + 253B48A714E341C500B0483F /* RestKit.framework */, + 253B48A914E341C500B0483F /* RestKitFrameworkTests.octest */, + 252A204315347F0B0078F8AD /* RestKitResources.bundle */, + ); + name = Products; + sourceTree = ""; + }; + 255207EE14E5AE2200EFD81F /* Models */ = { + isa = PBXGroup; + children = ( + 255207EF14E5AE2200EFD81F /* Data Model.xcdatamodel */, + 255207F014E5AE2200EFD81F /* RKCat.h */, + 255207F114E5AE2200EFD81F /* RKCat.m */, + 255207F214E5AE2200EFD81F /* RKChild.h */, + 255207F314E5AE2200EFD81F /* RKChild.m */, + 255207F414E5AE2200EFD81F /* RKDynamicMappingModels.h */, + 255207F514E5AE2200EFD81F /* RKDynamicMappingModels.m */, + 255207F614E5AE2200EFD81F /* RKEvent.h */, + 255207F714E5AE2200EFD81F /* RKEvent.m */, + 255207F814E5AE2200EFD81F /* RKHouse.h */, + 255207F914E5AE2200EFD81F /* RKHouse.m */, + 255207FA14E5AE2200EFD81F /* RKHuman.h */, + 255207FB14E5AE2200EFD81F /* RKHuman.m */, + 255207FC14E5AE2200EFD81F /* RKMappableAssociation.h */, + 255207FD14E5AE2200EFD81F /* RKMappableAssociation.m */, + 255207FE14E5AE2200EFD81F /* RKMappableObject.h */, + 255207FF14E5AE2200EFD81F /* RKMappableObject.m */, + 2552080014E5AE2200EFD81F /* RKObjectLoaderTestResultModel.h */, + 2552080114E5AE2200EFD81F /* RKObjectLoaderTestResultModel.m */, + 2552080214E5AE2200EFD81F /* RKObjectMapperTestModel.h */, + 2552080314E5AE2200EFD81F /* RKObjectMapperTestModel.m */, + 2552080414E5AE2200EFD81F /* RKParent.h */, + 2552080514E5AE2200EFD81F /* RKParent.m */, + 2552080614E5AE2200EFD81F /* RKResident.h */, + 2552080714E5AE2200EFD81F /* RKResident.m */, + 2552080814E5AE2200EFD81F /* RKSearchable.h */, + 2552080914E5AE2200EFD81F /* RKSearchable.m */, + 2552080A14E5AE2200EFD81F /* RKTestAddress.h */, + 2552080B14E5AE2200EFD81F /* RKTestAddress.m */, + 2552080C14E5AE2200EFD81F /* RKTestUser.h */, + 2552080D14E5AE2200EFD81F /* RKTestUser.m */, + ); + path = Models; + sourceTree = ""; + }; + 2552081E14E5AE4900EFD81F /* Vendor */ = { + isa = PBXGroup; + children = ( + 2552081F14E5AE4900EFD81F /* OCHamcrest */, + 2552082214E5AE4900EFD81F /* OCMock */, + ); + path = Vendor; + sourceTree = ""; + }; + 2552081F14E5AE4900EFD81F /* OCHamcrest */ = { + isa = PBXGroup; + children = ( + 2552082014E5AE4900EFD81F /* OCHamcrest.framework */, + 2552082114E5AE4900EFD81F /* OCHamcrestIOS.framework */, + ); + path = OCHamcrest; + sourceTree = ""; + }; + 2552082214E5AE4900EFD81F /* OCMock */ = { + isa = PBXGroup; + children = ( + 2552082314E5AE4900EFD81F /* OCMock */, + ); + path = OCMock; + sourceTree = ""; + }; + 2552082314E5AE4900EFD81F /* OCMock */ = { + isa = PBXGroup; + children = ( + 2552082414E5AE4900EFD81F /* InfoPlist.strings */, + 2552082614E5AE4900EFD81F /* NSInvocation+OCMAdditions.h */, + 2552082714E5AE4900EFD81F /* NSInvocation+OCMAdditions.m */, + 2552082814E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.h */, + 2552082914E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.m */, + 2552082A14E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.h */, + 2552082B14E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.m */, + 2552082C14E5AE4900EFD81F /* OCClassMockObject.h */, + 2552082D14E5AE4900EFD81F /* OCClassMockObject.m */, + 2552082E14E5AE4900EFD81F /* OCMArg.h */, + 2552082F14E5AE4900EFD81F /* OCMArg.m */, + 2552083014E5AE4900EFD81F /* OCMBlockCaller.h */, + 2552083114E5AE4900EFD81F /* OCMBlockCaller.m */, + 2552083214E5AE4900EFD81F /* OCMBoxedReturnValueProvider.h */, + 2552083314E5AE4900EFD81F /* OCMBoxedReturnValueProvider.m */, + 2552083414E5AE4900EFD81F /* OCMConstraint.h */, + 2552083514E5AE4900EFD81F /* OCMConstraint.m */, + 2552083614E5AE4900EFD81F /* OCMExceptionReturnValueProvider.h */, + 2552083714E5AE4900EFD81F /* OCMExceptionReturnValueProvider.m */, + 2552083814E5AE4900EFD81F /* OCMIndirectReturnValueProvider.h */, + 2552083914E5AE4900EFD81F /* OCMIndirectReturnValueProvider.m */, + 2552083A14E5AE4900EFD81F /* OCMNotificationPoster.h */, + 2552083B14E5AE4900EFD81F /* OCMNotificationPoster.m */, + 2552083C14E5AE4900EFD81F /* OCMObserverRecorder.h */, + 2552083D14E5AE4900EFD81F /* OCMObserverRecorder.m */, + 2552083E14E5AE4900EFD81F /* OCMock-Info.plist */, + 2552083F14E5AE4900EFD81F /* OCMock-Prefix.pch */, + 2552084014E5AE4900EFD81F /* OCMock.h */, + 2552084114E5AE4900EFD81F /* OCMockObject.h */, + 2552084214E5AE4900EFD81F /* OCMockObject.m */, + 2552084314E5AE4900EFD81F /* OCMockRecorder.h */, + 2552084414E5AE4900EFD81F /* OCMockRecorder.m */, + 2552084514E5AE4900EFD81F /* OCMPassByRefSetter.h */, + 2552084614E5AE4900EFD81F /* OCMPassByRefSetter.m */, + 2552084714E5AE4900EFD81F /* OCMRealObjectForwarder.h */, + 2552084814E5AE4900EFD81F /* OCMRealObjectForwarder.m */, + 2552084914E5AE4900EFD81F /* OCMReturnValueProvider.h */, + 2552084A14E5AE4900EFD81F /* OCMReturnValueProvider.m */, + 2552084B14E5AE4900EFD81F /* OCObserverMockObject.h */, + 2552084C14E5AE4900EFD81F /* OCObserverMockObject.m */, + 2552084D14E5AE4900EFD81F /* OCPartialMockObject.h */, + 2552084E14E5AE4900EFD81F /* OCPartialMockObject.m */, + 2552084F14E5AE4900EFD81F /* OCPartialMockRecorder.h */, + 2552085014E5AE4900EFD81F /* OCPartialMockRecorder.m */, + 2552085114E5AE4900EFD81F /* OCProtocolMockObject.h */, + 2552085214E5AE4900EFD81F /* OCProtocolMockObject.m */, + ); + path = OCMock; + sourceTree = ""; + }; + 25FABE7F14E3698100E609E7 /* Fixtures */ = { + isa = PBXGroup; + children = ( + 25FABE8014E3698100E609E7 /* Assets */, + 25FABE8214E3698100E609E7 /* JSON */, + 25FABE9E14E3698100E609E7 /* Uploads */, + 25FABEA114E3698100E609E7 /* XML */, + ); + path = Fixtures; + sourceTree = ""; + }; + 25FABE8014E3698100E609E7 /* Assets */ = { + isa = PBXGroup; + children = ( + 25FABE8114E3698100E609E7 /* blake.png */, + ); + path = Assets; + sourceTree = ""; + }; + 25FABE8214E3698100E609E7 /* JSON */ = { + isa = PBXGroup; + children = ( + 25FABE8314E3698100E609E7 /* ArrayOfNestedDictionaries.json */, + 25FABE8414E3698100E609E7 /* ArrayOfResults.json */, + 25FABE8514E3698100E609E7 /* ComplexNestedUser.json */, + 25FABE8614E3698100E609E7 /* ConnectingParents.json */, + 25FABE8714E3698100E609E7 /* Dynamic */, + 25FABE8E14E3698100E609E7 /* DynamicKeys.json */, + 25FABE8F14E3698100E609E7 /* DynamicKeysWithNestedRelationship.json */, + 25FABE9014E3698100E609E7 /* DynamicKeysWithRelationship.json */, + 25FABE9114E3698100E609E7 /* error.json */, + 25FABE9214E3698100E609E7 /* errors.json */, + 25FABE9314E3698100E609E7 /* Foursquare.json */, + 25FABE9414E3698100E609E7 /* humans */, + 25FABE9814E3698100E609E7 /* NakedEvents.json */, + 25FABE9914E3698100E609E7 /* nested_user.json */, + 25FABE9A14E3698100E609E7 /* RailsUser.json */, + 25FABE9B14E3698100E609E7 /* SameKeyDifferentTargetClasses.json */, + 25FABE9C14E3698100E609E7 /* user.json */, + 25FABE9D14E3698100E609E7 /* users.json */, + ); + path = JSON; + sourceTree = ""; + }; + 25FABE8714E3698100E609E7 /* Dynamic */ = { + isa = PBXGroup; + children = ( + 25FABE8814E3698100E609E7 /* boy.json */, + 25FABE8914E3698100E609E7 /* child.json */, + 25FABE8A14E3698100E609E7 /* friends.json */, + 25FABE8B14E3698100E609E7 /* girl.json */, + 25FABE8C14E3698100E609E7 /* mixed.json */, + 25FABE8D14E3698100E609E7 /* parent.json */, + ); + path = Dynamic; + sourceTree = ""; + }; + 25FABE9414E3698100E609E7 /* humans */ = { + isa = PBXGroup; + children = ( + 25FABE9514E3698100E609E7 /* 1.json */, + 25FABE9614E3698100E609E7 /* all.json */, + 25FABE9714E3698100E609E7 /* with_to_one_relationship.json */, + ); + path = humans; + sourceTree = ""; + }; + 25FABE9E14E3698100E609E7 /* Uploads */ = { + isa = PBXGroup; + children = ( + 25FABE9F14E3698100E609E7 /* .gitignore */, + 25FABEA014E3698100E609E7 /* file */, + ); + path = Uploads; + sourceTree = ""; + }; + 25FABEA114E3698100E609E7 /* XML */ = { + isa = PBXGroup; + children = ( + 25FABEA214E3698100E609E7 /* attributes_without_text_content.xml */, + 25FABEA314E3698100E609E7 /* channels.xml */, + 25FABEA414E3698100E609E7 /* container_attributes.xml */, + 25FABEA514E3698100E609E7 /* national_weather_service.xml */, + 25FABEA614E3698100E609E7 /* orders.xml */, + 25FABEA714E3698100E609E7 /* tab_data.xml */, + 25FABEA814E3698100E609E7 /* zend.xml */, + ); + path = XML; + sourceTree = ""; + }; + 25FCB3FE14E5B44C00A0FF3F /* UI */ = { + isa = PBXGroup; + children = ( + 25FCB3FF14E5B44C00A0FF3F /* RKFetchedResultsTableControllerTest.m */, + 25FCB40114E5B44C00A0FF3F /* RKFormTest.m */, + 25FCB40214E5B44C00A0FF3F /* RKTableControllerTest.m */, + 25FCB40314E5B44C00A0FF3F /* RKTableViewSectionTest.m */, + 25FCB40414E5B44C00A0FF3F /* RKTableViewCellMappingTest.m */, + 25FCB40514E5B44C00A0FF3F /* RKTableViewCellMappingsTest.m */, + ); + name = UI; + path = Application/UI; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 253B485E14E3415800B0483F /* RKApplicationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 253B489214E3415800B0483F /* Build configuration list for PBXNativeTarget "RKApplicationTests" */; + buildPhases = ( + 253B485B14E3415800B0483F /* Sources */, + 253B485C14E3415800B0483F /* Frameworks */, + 253B485D14E3415800B0483F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 253B48AB14E341DD00B0483F /* PBXTargetDependency */, + ); + name = RKApplicationTests; + productName = RKUITests; + productReference = 253B485F14E3415800B0483F /* RKApplicationTests.app */; + productType = "com.apple.product-type.application"; + }; + 253B487E14E3415800B0483F /* Application Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 253B489514E3415800B0483F /* Build configuration list for PBXNativeTarget "Application Tests" */; + buildPhases = ( + 253B487A14E3415800B0483F /* Sources */, + 253B487B14E3415800B0483F /* Frameworks */, + 253B487C14E3415800B0483F /* Resources */, + 253B487D14E3415800B0483F /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 253B488614E3415800B0483F /* PBXTargetDependency */, + ); + name = "Application Tests"; + productName = RKUITestsTests; + productReference = 253B487F14E3415800B0483F /* Application Tests.octest */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 253B485614E3415800B0483F /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0430; + ORGANIZATIONNAME = RestKit; + }; + buildConfigurationList = 253B485914E3415800B0483F /* Build configuration list for PBXProject "RKApplicationTests" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 253B485414E3415800B0483F; + productRefGroup = 253B486014E3415800B0483F /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 253B489914E341C500B0483F /* Products */; + ProjectRef = 253B489814E341C500B0483F /* RestKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 253B485E14E3415800B0483F /* RKApplicationTests */, + 253B487E14E3415800B0483F /* Application Tests */, + ); + }; +/* 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; + path = libRestKit.a; + remoteRef = 253B48A214E341C500B0483F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 253B48A514E341C500B0483F /* RestKitTests.octest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitTests.octest; + remoteRef = 253B48A414E341C500B0483F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 253B48A714E341C500B0483F /* RestKit.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = RestKit.framework; + remoteRef = 253B48A614E341C500B0483F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 253B48A914E341C500B0483F /* RestKitFrameworkTests.octest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = RestKitFrameworkTests.octest; + remoteRef = 253B48A814E341C500B0483F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 253B485D14E3415800B0483F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 253B487014E3415800B0483F /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 253B487C14E3415800B0483F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 253B488C14E3415800B0483F /* InfoPlist.strings in Resources */, + 25FABEA914E3698100E609E7 /* blake.png in Resources */, + 25FABEAA14E3698100E609E7 /* ArrayOfNestedDictionaries.json in Resources */, + 25FABEAB14E3698100E609E7 /* ArrayOfResults.json in Resources */, + 25FABEAC14E3698100E609E7 /* ComplexNestedUser.json in Resources */, + 25FABEAD14E3698100E609E7 /* ConnectingParents.json in Resources */, + 25FABEAE14E3698100E609E7 /* boy.json in Resources */, + 25FABEAF14E3698100E609E7 /* child.json in Resources */, + 25FABEB014E3698100E609E7 /* friends.json in Resources */, + 25FABEB114E3698100E609E7 /* girl.json in Resources */, + 25FABEB214E3698100E609E7 /* mixed.json in Resources */, + 25FABEB314E3698100E609E7 /* parent.json in Resources */, + 25FABEB414E3698100E609E7 /* DynamicKeys.json in Resources */, + 25FABEB514E3698100E609E7 /* DynamicKeysWithNestedRelationship.json in Resources */, + 25FABEB614E3698100E609E7 /* DynamicKeysWithRelationship.json in Resources */, + 25FABEB714E3698100E609E7 /* error.json in Resources */, + 25FABEB814E3698100E609E7 /* errors.json in Resources */, + 25FABEB914E3698100E609E7 /* Foursquare.json in Resources */, + 25FABEBA14E3698100E609E7 /* 1.json in Resources */, + 25FABEBB14E3698100E609E7 /* all.json in Resources */, + 25FABEBC14E3698100E609E7 /* with_to_one_relationship.json in Resources */, + 25FABEBD14E3698100E609E7 /* NakedEvents.json in Resources */, + 25FABEBE14E3698100E609E7 /* nested_user.json in Resources */, + 25FABEBF14E3698100E609E7 /* RailsUser.json in Resources */, + 25FABEC014E3698100E609E7 /* SameKeyDifferentTargetClasses.json in Resources */, + 25FABEC114E3698100E609E7 /* user.json in Resources */, + 25FABEC214E3698100E609E7 /* users.json in Resources */, + 25FABEC314E3698100E609E7 /* .gitignore in Resources */, + 25FABEC414E3698100E609E7 /* file in Resources */, + 25FABEC514E3698100E609E7 /* attributes_without_text_content.xml in Resources */, + 25FABEC614E3698100E609E7 /* channels.xml in Resources */, + 25FABEC714E3698100E609E7 /* container_attributes.xml in Resources */, + 25FABEC814E3698100E609E7 /* national_weather_service.xml in Resources */, + 25FABEC914E3698100E609E7 /* orders.xml in Resources */, + 25FABECA14E3698100E609E7 /* tab_data.xml in Resources */, + 25FABECB14E3698100E609E7 /* zend.xml in Resources */, + 2552085514E5AE4900EFD81F /* InfoPlist.strings in Resources */, + 2552086214E5AE4900EFD81F /* OCMock-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 253B487D14E3415800B0483F /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the unit tests in this test bundle.\n\"${SRCROOT}/../RunPlatformUnitTests\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 253B485B14E3415800B0483F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 253B487214E3415800B0483F /* main.m in Sources */, + 253B487614E3415800B0483F /* RKATAppDelegate.m in Sources */, + 255207ED14E5A50800EFD81F /* RKATTests.xcdatamodeld in Sources */, + 25FCB40D14E5B5BC00A0FF3F /* Data Model.xcdatamodel in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 253B487A14E3415800B0483F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2552080F14E5AE2200EFD81F /* RKCat.m in Sources */, + 2552081014E5AE2200EFD81F /* RKChild.m in Sources */, + 2552081114E5AE2200EFD81F /* RKDynamicMappingModels.m in Sources */, + 2552081214E5AE2200EFD81F /* RKEvent.m in Sources */, + 2552081314E5AE2200EFD81F /* RKHouse.m in Sources */, + 2552081414E5AE2200EFD81F /* RKHuman.m in Sources */, + 2552081514E5AE2200EFD81F /* RKMappableAssociation.m in Sources */, + 2552081614E5AE2200EFD81F /* RKMappableObject.m in Sources */, + 2552081714E5AE2200EFD81F /* RKObjectLoaderTestResultModel.m in Sources */, + 2552081814E5AE2200EFD81F /* RKObjectMapperTestModel.m in Sources */, + 2552081914E5AE2200EFD81F /* RKParent.m in Sources */, + 2552081A14E5AE2200EFD81F /* RKResident.m in Sources */, + 2552081B14E5AE2200EFD81F /* RKSearchable.m in Sources */, + 2552081C14E5AE2200EFD81F /* RKTestAddress.m in Sources */, + 2552081D14E5AE2200EFD81F /* RKTestUser.m in Sources */, + 2552085614E5AE4900EFD81F /* NSInvocation+OCMAdditions.m in Sources */, + 2552085714E5AE4900EFD81F /* NSMethodSignature+OCMAdditions.m in Sources */, + 2552085814E5AE4900EFD81F /* NSNotificationCenter+OCMAdditions.m in Sources */, + 2552085914E5AE4900EFD81F /* OCClassMockObject.m in Sources */, + 2552085A14E5AE4900EFD81F /* OCMArg.m in Sources */, + 2552085B14E5AE4900EFD81F /* OCMBlockCaller.m in Sources */, + 2552085C14E5AE4900EFD81F /* OCMBoxedReturnValueProvider.m in Sources */, + 2552085D14E5AE4900EFD81F /* OCMConstraint.m in Sources */, + 2552085E14E5AE4900EFD81F /* OCMExceptionReturnValueProvider.m in Sources */, + 2552085F14E5AE4900EFD81F /* OCMIndirectReturnValueProvider.m in Sources */, + 2552086014E5AE4900EFD81F /* OCMNotificationPoster.m in Sources */, + 2552086114E5AE4900EFD81F /* OCMObserverRecorder.m in Sources */, + 2552086314E5AE4900EFD81F /* OCMockObject.m in Sources */, + 2552086414E5AE4900EFD81F /* OCMockRecorder.m in Sources */, + 2552086514E5AE4900EFD81F /* OCMPassByRefSetter.m in Sources */, + 2552086614E5AE4900EFD81F /* OCMRealObjectForwarder.m in Sources */, + 2552086714E5AE4900EFD81F /* OCMReturnValueProvider.m in Sources */, + 2552086814E5AE4900EFD81F /* OCObserverMockObject.m in Sources */, + 2552086914E5AE4900EFD81F /* OCPartialMockObject.m in Sources */, + 2552086A14E5AE4900EFD81F /* OCPartialMockRecorder.m in Sources */, + 2552086B14E5AE4900EFD81F /* OCProtocolMockObject.m in Sources */, + 2552087E14E5B0B500EFD81F /* RKTestEnvironment.m in Sources */, + 25FCB40614E5B44C00A0FF3F /* RKFetchedResultsTableControllerTest.m in Sources */, + 25FCB40814E5B44C00A0FF3F /* RKFormTest.m in Sources */, + 25FCB40914E5B44C00A0FF3F /* RKTableControllerTest.m in Sources */, + 25FCB40A14E5B44C00A0FF3F /* RKTableViewSectionTest.m in Sources */, + 25FCB40B14E5B44C00A0FF3F /* RKTableViewCellMappingTest.m in Sources */, + 25FCB40C14E5B44C00A0FF3F /* RKTableViewCellMappingsTest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 253B488614E3415800B0483F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 253B485E14E3415800B0483F /* RKApplicationTests */; + targetProxy = 253B488514E3415800B0483F /* PBXContainerItemProxy */; + }; + 253B48AB14E341DD00B0483F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = RestKit; + targetProxy = 253B48AA14E341DD00B0483F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 253B486E14E3415800B0483F /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 253B486F14E3415800B0483F /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 253B488A14E3415800B0483F /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 253B488B14E3415800B0483F /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 2552082414E5AE4900EFD81F /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 2552082514E5AE4900EFD81F /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 253B489014E3415800B0483F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 253B489114E3415800B0483F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 253B489314E3415800B0483F /* Debug */ = { + isa = XCBuildConfiguration; + 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)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 253B489414E3415800B0483F /* Release */ = { + isa = XCBuildConfiguration; + 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)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 253B489614E3415800B0483F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/RKApplicationTests.app/RKApplicationTests"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_LIBRARY_DIR)/Frameworks", + "\"$(SRCROOT)/../Vendor\"", + "\"$(SRCROOT)/../Vendor/OCHamcrest\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "RKApplicationTests/App/RKApplicationTests-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../Models", + "$(SRCROOT)/..", + "\"$(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 = "\"$(BUILT_PRODUCTS_DIR)/../../Headers/RestKit\""; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 253B489714E3415800B0483F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/RKApplicationTests.app/RKApplicationTests"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_LIBRARY_DIR)/Frameworks", + "\"$(SRCROOT)/../Vendor\"", + "\"$(SRCROOT)/../Vendor/OCHamcrest\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "RKApplicationTests/App/RKApplicationTests-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../Models", + "$(SRCROOT)/..", + "\"$(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 = "\"$(BUILT_PRODUCTS_DIR)/../../Headers/RestKit\""; + WRAPPER_EXTENSION = octest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 253B485914E3415800B0483F /* Build configuration list for PBXProject "RKApplicationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 253B489014E3415800B0483F /* Debug */, + 253B489114E3415800B0483F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 253B489214E3415800B0483F /* Build configuration list for PBXNativeTarget "RKApplicationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 253B489314E3415800B0483F /* Debug */, + 253B489414E3415800B0483F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 253B489514E3415800B0483F /* Build configuration list for PBXNativeTarget "Application Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 253B489614E3415800B0483F /* Debug */, + 253B489714E3415800B0483F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 255207EB14E5A50800EFD81F /* RKATTests.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 255207EC14E5A50800EFD81F /* RKUITests.xcdatamodel */, + ); + currentVersion = 255207EC14E5A50800EFD81F /* RKUITests.xcdatamodel */; + path = RKATTests.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 253B485614E3415800B0483F /* Project object */; +} diff --git a/Tests/Application/RKApplicationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/Application/RKApplicationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..f97aa96086 --- /dev/null +++ b/Tests/Application/RKApplicationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Tests/Application/RKApplicationTests/App/RKATAppDelegate.h b/Tests/Application/RKApplicationTests/App/RKATAppDelegate.h new file mode 100644 index 0000000000..7bfe2399fe --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKATAppDelegate.h @@ -0,0 +1,22 @@ +// +// RKATAppDelegate.h +// RKApplicationTests +// +// Created by Blake Watters on 2/8/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +@interface RKATAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; +@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; +@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +- (void)saveContext; +- (NSURL *)applicationDocumentsDirectory; + +@end diff --git a/Tests/Application/RKApplicationTests/App/RKATAppDelegate.m b/Tests/Application/RKApplicationTests/App/RKATAppDelegate.m new file mode 100644 index 0000000000..861b3ce94a --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKATAppDelegate.m @@ -0,0 +1,190 @@ +// +// RKATAppDelegate.m +// RKApplicationTests +// +// Created by Blake Watters on 2/8/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKATAppDelegate.h" + +@implementation RKATAppDelegate + +@synthesize window = _window; +@synthesize managedObjectContext = __managedObjectContext; +@synthesize managedObjectModel = __managedObjectModel; +@synthesize persistentStoreCoordinator = __persistentStoreCoordinator; + +- (void)dealloc +{ + [_window release]; + [__managedObjectContext release]; + [__managedObjectModel release]; + [__persistentStoreCoordinator release]; + [super dealloc]; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease]; + // Override point for customization after application launch. + self.window.backgroundColor = [UIColor whiteColor]; + [self.window makeKeyAndVisible]; + + UITableViewController *tableViewController = [[UITableViewController alloc] initWithStyle:UITableViewStylePlain]; + self.window.rootViewController = tableViewController; + [tableViewController release]; + + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + /* + Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + */ +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + /* + Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + */ +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + /* + Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + */ +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + /* + Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + */ +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + // Saves changes in the application's managed object context before the application terminates. + [self saveContext]; +} + +- (void)saveContext +{ + NSError *error = nil; + NSManagedObjectContext *managedObjectContext = self.managedObjectContext; + if (managedObjectContext != nil) + { + if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) + { + /* + Replace this implementation with code to handle the error appropriately. + + abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + */ + NSLog(@"Unresolved error %@, %@", error, [error userInfo]); + abort(); + } + } +} + +#pragma mark - Core Data stack + +/** + Returns the managed object context for the application. + If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application. + */ +- (NSManagedObjectContext *)managedObjectContext +{ + if (__managedObjectContext != nil) + { + return __managedObjectContext; + } + + NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; + if (coordinator != nil) + { + __managedObjectContext = [[NSManagedObjectContext alloc] init]; + [__managedObjectContext setPersistentStoreCoordinator:coordinator]; + } + return __managedObjectContext; +} + +/** + Returns the managed object model for the application. + If the model doesn't already exist, it is created from the application's model. + */ +- (NSManagedObjectModel *)managedObjectModel +{ + if (__managedObjectModel != nil) + { + return __managedObjectModel; + } + NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"RKApplicationTests" withExtension:@"momd"]; + __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; + return __managedObjectModel; +} + +/** + Returns the persistent store coordinator for the application. + If the coordinator doesn't already exist, it is created and the application's store added to it. + */ +- (NSPersistentStoreCoordinator *)persistentStoreCoordinator +{ + if (__persistentStoreCoordinator != nil) + { + return __persistentStoreCoordinator; + } + + NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"RKApplicationTests.sqlite"]; + + NSError *error = nil; + __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; + if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) + { + /* + Replace this implementation with code to handle the error appropriately. + + abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + Typical reasons for an error here include: + * The persistent store is not accessible; + * The schema for the persistent store is incompatible with current managed object model. + Check the error message to determine what the actual problem was. + + + If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory. + + If you encounter schema incompatibility errors during development, you can reduce their frequency by: + * Simply deleting the existing store: + [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil] + + * Performing automatic lightweight migration by passing the following dictionary as the options parameter: + [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; + + Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details. + + */ + NSLog(@"Unresolved error %@, %@", error, [error userInfo]); + abort(); + } + + return __persistentStoreCoordinator; +} + +#pragma mark - Application's Documents directory + +/** + Returns the URL to the application's Documents directory. + */ +- (NSURL *)applicationDocumentsDirectory +{ + return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; +} + +@end diff --git a/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/.xccurrentversion b/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000000..f37ca0c92a --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + RKUITests.xcdatamodel + + diff --git a/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/RKUITests.xcdatamodel/contents b/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/RKUITests.xcdatamodel/contents new file mode 100644 index 0000000000..193f33c9c4 --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKATTests.xcdatamodeld/RKUITests.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Tests/Application/RKApplicationTests/App/RKApplicationTests-Info.plist b/Tests/Application/RKApplicationTests/App/RKApplicationTests-Info.plist new file mode 100644 index 0000000000..3b21097a60 --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKApplicationTests-Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFiles + + CFBundleIdentifier + org.restkit.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Tests/Application/RKApplicationTests/App/RKApplicationTests-Prefix.pch b/Tests/Application/RKApplicationTests/App/RKApplicationTests-Prefix.pch new file mode 100644 index 0000000000..0d2ea87a86 --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/RKApplicationTests-Prefix.pch @@ -0,0 +1,15 @@ +// +// Prefix header for all source files of the 'RKApplicationTests' target in the 'RKApplicationTests' project +// + +#import + +#ifndef __IPHONE_3_0 +#warning "This project uses features only available in iOS SDK 3.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import + #import +#endif diff --git a/Tests/Application/RKApplicationTests/App/en.lproj/InfoPlist.strings b/Tests/Application/RKApplicationTests/App/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b92732c79e --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/Tests/Application/RKApplicationTests/App/main.m b/Tests/Application/RKApplicationTests/App/main.m new file mode 100644 index 0000000000..dd3f62dd22 --- /dev/null +++ b/Tests/Application/RKApplicationTests/App/main.m @@ -0,0 +1,18 @@ +// +// main.m +// RKApplicationTests +// +// Created by Blake Watters on 2/8/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +#import "RKATAppDelegate.h" + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([RKATAppDelegate class])); + } +} diff --git a/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist b/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist new file mode 100644 index 0000000000..883b278e89 --- /dev/null +++ b/Tests/Application/RKApplicationTests/Tests/RKApplicationTestsTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + org.restkit.tests + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Tests/Application/RKApplicationTests/Tests/en.lproj/InfoPlist.strings b/Tests/Application/RKApplicationTests/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b92732c79e --- /dev/null +++ b/Tests/Application/RKApplicationTests/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/Tests/Application/UI/RKFetchedResultsTableControllerTest.m b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m new file mode 100644 index 0000000000..70430320e0 --- /dev/null +++ b/Tests/Application/UI/RKFetchedResultsTableControllerTest.m @@ -0,0 +1,1467 @@ +// +// RKFetchedResultsTableControllerTest.m +// RestKit +// +// Created by Jeff Arena on 8/12/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKFetchedResultsTableController.h" +#import "RKManagedObjectStore.h" +#import "RKManagedObjectMapping.h" +#import "RKHuman.h" +#import "RKEvent.h" +#import "RKAbstractTableController_Internals.h" +#import "RKManagedObjectCaching.h" +#import "RKTableControllerTestDelegate.h" + +// Expose the object loader delegate for testing purposes... +@interface RKFetchedResultsTableController () + +- (BOOL)isHeaderSection:(NSUInteger)section; +- (BOOL)isHeaderRow:(NSUInteger)row; +- (BOOL)isFooterSection:(NSUInteger)section; +- (BOOL)isFooterRow:(NSUInteger)row; +- (BOOL)isEmptySection:(NSUInteger)section; +- (BOOL)isEmptyRow:(NSUInteger)row; +- (BOOL)isHeaderIndexPath:(NSIndexPath*)indexPath; +- (BOOL)isFooterIndexPath:(NSIndexPath*)indexPath; +- (BOOL)isEmptyItemIndexPath:(NSIndexPath*)indexPath; +- (NSIndexPath*)fetchedResultsIndexPathForIndexPath:(NSIndexPath*)indexPath; + +@end + +@interface RKFetchedResultsTableControllerSpecViewController : UITableViewController +@end + +@implementation RKFetchedResultsTableControllerSpecViewController +@end + +@interface RKFetchedResultsTableControllerTest : RKTestCase +@end + +@implementation RKFetchedResultsTableControllerTest + +- (void)setUp { + [RKTestFactory setUp]; + + [[[[UIApplication sharedApplication] windows] objectAtIndex:0] setRootViewController:nil]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + +- (void)bootstrapStoreAndCache { + 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]; + blake.railsID = [NSNumber numberWithInt:1234]; + blake.name = @"blake"; + RKHuman* other = [RKHuman createEntity]; + other.railsID = [NSNumber numberWithInt:5678]; + other.name = @"other"; + NSError* error = nil; + [store save:&error]; + assertThat(error, is(nilValue())); + assertThatInt([RKHuman count:nil], is(equalToInt(2))); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; + objectManager.objectStore = store; + + [objectManager.mappingProvider setObjectMapping:humanMapping forResourcePathPattern:@"/JSON/humans/all\\.json" withFetchRequestBlock:^NSFetchRequest *(NSString *resourcePath) { + return [RKHuman requestAllSortedBy:@"name" ascending:YES]; + }]; +} + +- (void)bootstrapNakedObjectStoreAndCache { + 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 = nil; + [store save:&error]; + assertThat(error, is(nilValue())); + assertThatInt([RKEvent count:nil], is(equalToInt(1))); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.mappingProvider addObjectMapping:eventMapping]; + objectManager.objectStore = store; + + id mockMappingProvider = [OCMockObject partialMockForObject:objectManager.mappingProvider]; + [[[mockMappingProvider stub] andReturn:[RKEvent requestAllSortedBy:@"eventType" ascending:YES]] fetchRequestForResourcePath:@"/JSON/NakedEvents.json"]; +} + +- (void)bootstrapEmptyStoreAndCache { + 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 = [RKTestFactory objectManager]; + [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; + objectManager.objectStore = store; + + 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 { + RKObjectManager *objectManager = [RKObjectManager sharedManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + [mockManager setExpectationOrderMatters:YES]; + RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(networkStatus)] networkStatus]; + BOOL online = YES; // Initial online state for table + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; +} + +- (void)testLoadWithATableViewControllerAndResourcePath { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + + assertThat(tableController.viewController, is(equalTo(viewController))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); + assertThat(tableController.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); +} + +- (void)testLoadWithATableViewControllerAndResourcePathFromNakedObjects { + [self bootstrapNakedObjectStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + 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(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"))); +} + + +- (void)testLoadWithATableViewControllerAndResourcePathAndPredicateAndSortDescriptors { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; + NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" + ascending:YES]]; + 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.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); + assertThat(tableController.fetchRequest, is(notNilValue())); + assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); + assertThat([tableController.fetchRequest sortDescriptors], is(equalTo(sortDescriptors))); +} + +- (void)testLoadWithATableViewControllerAndResourcePathAndSectionNameAndCacheName { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + 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.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); + assertThat(tableController.fetchRequest, is(notNilValue())); + assertThat(tableController.fetchedResultsController.sectionNameKeyPath, is(equalTo(@"name"))); + assertThat(tableController.fetchedResultsController.cacheName, is(equalTo(@"allHumansCache"))); +} + +- (void)testLoadWithAllParams { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE]; + NSArray* sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" + ascending:YES]]; + 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.resourcePath, is(equalTo(@"/JSON/humans/all.json"))); + assertThat(tableController.fetchRequest, is(notNilValue())); + assertThat([tableController.fetchRequest predicate], is(equalTo(predicate))); + assertThat([tableController.fetchRequest sortDescriptors], is(equalTo(sortDescriptors))); + assertThat(tableController.fetchedResultsController.sectionNameKeyPath, is(equalTo(@"name"))); + assertThat(tableController.fetchedResultsController.cacheName, is(equalTo(@"allHumansCache"))); +} + +- (void)testAlwaysHaveAtLeastOneSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + + assertThatInt(tableController.sectionCount, is(equalToInt(1))); +} + +#pragma mark - Section Management + +- (void)testProperlyCountSections { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController loadTable]; + assertThatInt(tableController.sectionCount, is(equalToInt(2))); +} + +- (void)testProperlyCountRows { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(2))); +} + +- (void)testProperlyCountRowsWithHeaderItems { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(3))); +} + +- (void)testProperlyCountRowsWithEmptyItemWhenEmpty { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(1))); +} + +- (void)testProperlyCountRowsWithEmptyItemWhenFull { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(2))); +} + +- (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyDontShowHeaders { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsHeaderRowsWhenEmpty = NO; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(1))); +} + +- (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenEmptyShowHeaders { + [self bootstrapEmptyStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsHeaderRowsWhenEmpty = YES; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(2))); +} + +- (void)testProperlyCountRowsWithHeaderAndEmptyItemsWhenFull { + [self bootstrapStoreAndCache]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerForTableViewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatInt([tableController rowCount], is(equalToInt(3))); +} + +#pragma mark - UITableViewDataSource specs + +- (void)testRaiseAnExceptionIfSentAMessageWithATableViewItIsNotBoundTo { + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = [RKFetchedResultsTableController tableControllerWithTableView:tableView forViewController:viewController]; + NSException* exception = nil; + @try { + [tableController numberOfSectionsInTableView:[UITableView new]]; + } + @catch (NSException* e) { + exception = e; + } + @finally { + assertThat(exception, is(notNilValue())); + } +} + +- (void)testReturnTheNumberOfSectionsInTableView { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController loadTable]; + + assertThatInt([tableController numberOfSectionsInTableView:tableView], is(equalToInt(2))); +} + +- (void)testReturnTheNumberOfRowsInSectionInTableView { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + + assertThatInt([tableController tableView:tableView numberOfRowsInSection:0], is(equalToInt(2))); +} + +- (void)testReturnTheHeaderTitleForSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController loadTable]; + + assertThat([tableController tableView:tableView titleForHeaderInSection:1], is(equalTo(@"other"))); +} + +- (void)testReturnTheTableViewCellForRowAtIndexPath { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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"))); +} + +#pragma mark - Table Cell Mapping + +- (void)testReturnTheObjectForARowAtIndexPath { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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 + +- (void)testFireADeleteRequestWhenTheCanEditRowsPropertyIsSet { + [self bootstrapStoreAndCache]; + [self stubObjectManagerToOnline]; + [[RKObjectManager sharedManager].router routeClass:[RKHuman class] + toResourcePath:@"/humans/:railsID" + forMethod:RKRequestMethodDELETE]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + 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]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); + + [RKTestNotificationObserver waitForNotificationWithName:RKRequestDidLoadResponseNotification usingBlock:^{ + [tableController tableView:tableController.tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; + }]; + + assertThatInt([tableController rowCount], is(equalToInt(1))); + assertThat([tableController objectForRowAtIndexPath:deleteIndexPath], is(equalTo(other))); + assertThatBool([blake isDeleted], is(equalToBool(YES))); +} + +- (void)testLocallyCommitADeleteWhenTheCanEditRowsPropertyIsSet { + [self bootstrapStoreAndCache]; + [self stubObjectManagerToOnline]; + + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:deleteIndexPath]; + assertThatInt([tableController rowCount], is(equalToInt(1))); + assertThat([tableController objectForRowAtIndexPath:indexPath], is(equalTo(blake))); +} + +- (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { + [self bootstrapStoreAndCache]; + [self stubObjectManagerToOnline]; + + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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]; + assertThatBool(delegateCanEdit, is(equalToBool(NO))); + [tableController tableView:tableController.tableView + 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]], + is(equalTo(other))); +} + +- (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { + [self bootstrapStoreAndCache]; + [self stubObjectManagerToOnline]; + + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + 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]], + is(equalTo(other))); +} + +- (void)testNotMoveARowWhenTheCanMoveRowsPropertyIsSet { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + 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]]; + assertThatBool(delegateCanMove, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + 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]], + is(equalTo(other))); +} + +#pragma mark - Header, Footer, and Empty Rows + +- (void)testDetermineIfASectionIndexIsAHeaderSection { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + assertThatBool([tableController isHeaderSection:0], is(equalToBool(YES))); + assertThatBool([tableController isHeaderSection:1], is(equalToBool(NO))); + assertThatBool([tableController isHeaderSection:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfARowIndexIsAHeaderRow { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isHeaderRow:0], is(equalToBool(YES))); + assertThatBool([tableController isHeaderRow:1], is(equalToBool(NO))); + assertThatBool([tableController isHeaderRow:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfASectionIndexIsAFooterSectionSingleSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isFooterSection:0], is(equalToBool(YES))); + assertThatBool([tableController isFooterSection:1], is(equalToBool(NO))); + assertThatBool([tableController isFooterSection:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfASectionIndexIsAFooterSectionMultipleSections { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isFooterSection:0], is(equalToBool(NO))); + assertThatBool([tableController isFooterSection:1], is(equalToBool(YES))); + assertThatBool([tableController isFooterSection:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfARowIndexIsAFooterRow { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isFooterRow:0], is(equalToBool(NO))); + assertThatBool([tableController isFooterRow:1], is(equalToBool(NO))); + assertThatBool([tableController isFooterRow:2], is(equalToBool(YES))); +} + +- (void)testDetermineIfASectionIndexIsAnEmptySection { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + assertThatBool([tableController isEmptySection:0], is(equalToBool(YES))); + assertThatBool([tableController isEmptySection:1], is(equalToBool(NO))); + assertThatBool([tableController isEmptySection:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfARowIndexIsAnEmptyRow { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController loadTable]; + assertThatBool([tableController isEmptyRow:0], is(equalToBool(YES))); + assertThatBool([tableController isEmptyRow:1], is(equalToBool(NO))); + assertThatBool([tableController isEmptyRow:2], is(equalToBool(NO))); +} + +- (void)testDetermineIfAnIndexPathIsAHeaderIndexPath { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalToBool(YES))); + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isHeaderIndexPath:[NSIndexPath indexPathForRow:2 inSection:1]], is(equalToBool(NO))); +} + +- (void)testDetermineIfAnIndexPathIsAFooterIndexPath { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalToBool(YES))); + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isFooterIndexPath:[NSIndexPath indexPathForRow:2 inSection:1]], is(equalToBool(NO))); +} + +- (void)testDetermineIfAnIndexPathIsAnEmptyIndexPathSingleSectionEmptyItemOnly { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalToBool(YES))); + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalToBool(NO))); + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalToBool(NO))); + assertThatBool([tableController isEmptyItemIndexPath:[NSIndexPath indexPathForRow:2 inSection:1]], is(equalToBool(NO))); +} + +- (void)testConvertAnIndexPathForHeaderRows { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); +} + +- (void)testConvertAnIndexPathForFooterRowsSingleSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); +} + +- (void)testConvertAnIndexPathForFooterRowsMultipleSections { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:1]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:1]))); +} + +- (void)testConvertAnIndexPathForEmptyRow { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:3 inSection:0]))); +} + +- (void)testConvertAnIndexPathForHeaderFooterRowsSingleSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); +} + +- (void)testConvertAnIndexPathForHeaderFooterRowsMultipleSections { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:1]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:1]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:1]))); +} + +- (void)testConvertAnIndexPathForHeaderFooterEmptyRowsSingleSection { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:4 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:3 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:5 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:4 inSection:0]))); +} + +- (void)testConvertAnIndexPathForHeaderFooterEmptyRowsMultipleSections { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + tableController.sectionNameKeyPath = @"name"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:1]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:1]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:1]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:1]))); +} + +- (void)testConvertAnIndexPathForHeaderEmptyRows { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController loadTable]; + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:0 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:1 inSection:0]))); + assertThat([tableController fetchedResultsIndexPathForIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], is(equalTo([NSIndexPath indexPathForRow:2 inSection:0]))); +} + +- (void)testShowHeaderRows { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]; + [tableController addHeaderRowForItem:headerRow]; + 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))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], + is(equalTo(other))); +} + +- (void)testShowFooterRows { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]; + [tableController addFooterRowForItem:footerRow]; + 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))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], + is(equalTo(footerRow))); +} + +- (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + 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))); +} + +- (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + 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))); +} + +- (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + 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))); +} + +- (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { + [self bootstrapStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + + RKTableItem* headerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]; + [tableController addHeaderRowForItem:headerRow]; + + RKTableItem* footerRow = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]; + [tableController addFooterRowForItem:footerRow]; + + RKTableItem* emptyItem = [RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]; + [tableController setEmptyItem:emptyItem]; + 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))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]], is(equalTo(other))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]], + is(equalTo(footerRow))); +} + +- (void)testShowTheEmptyItemWhenTheTableIsEmpty { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + 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))); +} + +- (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { + [self bootstrapEmptyStoreAndCache]; + UITableView* tableView = [UITableView new]; + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = + [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + tableController.resourcePath = @"/JSON/humans/all.json"; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + 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))); +} + +- (void)testShowTheEmptyImageAfterLoadingAnEmptyCollectionIntoAnEmptyFetch { + [self bootstrapEmptyStoreAndCache]; + [self stubObjectManagerToOnline]; + + UITableView* tableView = [UITableView new]; + + RKFetchedResultsTableControllerSpecViewController* viewController = [RKFetchedResultsTableControllerSpecViewController new]; + RKFetchedResultsTableController* tableController = [[RKFetchedResultsTableController alloc] initWithTableView:tableView + viewController:viewController]; + + UIImage *image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + + tableController.imageForEmpty = image; + tableController.resourcePath = @"/empty/array"; + tableController.autoRefreshFromNetwork = YES; + [tableController.cache invalidateAll]; + + [RKTestNotificationObserver waitForNotificationWithName:RKTableControllerDidFinishLoadNotification usingBlock:^{ + [tableController loadTable]; + }]; + assertThatBool(tableController.isLoaded, is(equalToBool(YES))); + assertThatInt([tableController rowCount], is(equalToInt(0))); + assertThatBool(tableController.isEmpty, is(equalToBool(YES))); + 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/RKFormTest.m b/Tests/Application/UI/RKFormTest.m new file mode 100644 index 0000000000..d5554e779a --- /dev/null +++ b/Tests/Application/UI/RKFormTest.m @@ -0,0 +1,126 @@ +// +// RKFormTest.m +// RestKit +// +// Created by Blake Watters on 8/29/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKForm.h" +#import "RKMappableObject.h" +#import "RKTableController.h" + +@interface UISwitch (ControlValue) +@property (nonatomic, assign) NSNumber *controlValue; +@end + +@implementation UISwitch (ControlValue) + +- (NSNumber *)controlValue { + return [NSNumber numberWithBool:self.isOn]; +} + +- (void)setControlValue:(NSNumber *)controlValue { + self.on = [controlValue boolValue]; +} + +@end + +@interface RKTestTextField : UIControl +@property (nonatomic, retain) NSString *text; +@end + +@implementation RKTestTextField + +@synthesize text; + +@end + +/////////////////////////////////////////////////////////////// + +@interface RKFormSpecTableViewCell : UITableViewCell { +} + +@property (nonatomic, retain) NSString *someTextProperty; + +@end + +@implementation RKFormSpecTableViewCell + +@synthesize someTextProperty; + +@end + +/////////////////////////////////////////////////////////////// + +@interface RKFormTest : RKTestCase + +@end + +@implementation RKFormTest + +- (void)testCommitValuesBackToTheFormObjectWithBuiltInTypes { + RKMappableObject *mappableObject = [[RKMappableObject new] autorelease]; + RKForm *form = [RKForm formForObject:mappableObject usingBlock:^(RKForm *form) { + [form addRowForAttribute:@"stringTest" withControlType:RKFormControlTypeTextField usingBlock:^(RKControlTableItem *tableItem) { + tableItem.textField.text = @"testing 123"; + }]; + [form addRowForAttribute:@"numberTest" withControlType:RKFormControlTypeSwitch usingBlock:^(RKControlTableItem *tableItem) { + tableItem.switchControl.on = YES; + }]; + }]; + [form commitValuesToObject]; + assertThat(mappableObject.stringTest, is(equalTo(@"testing 123"))); + assertThatBool([mappableObject.numberTest boolValue], is(equalToBool(YES))); +} + +- (void)testCommitValuesBackToTheFormObjectFromUserConfiguredControls { + RKTestTextField *textField = [[RKTestTextField new] autorelease]; + textField.text = @"testing 123"; + UISwitch *switchControl = [[UISwitch new] autorelease]; + switchControl.on = YES; + RKMappableObject *mappableObject = [[RKMappableObject new] autorelease]; + RKForm *form = [RKForm formForObject:mappableObject usingBlock:^(RKForm *form) { + [form addRowMappingAttribute:@"stringTest" toKeyPath:@"text" onControl:textField]; + [form addRowMappingAttribute:@"numberTest" toKeyPath:@"controlValue" onControl:switchControl]; + }]; + [form commitValuesToObject]; + assertThat(mappableObject.stringTest, is(equalTo(@"testing 123"))); + assertThatBool([mappableObject.numberTest boolValue], is(equalToBool(YES))); +} + +- (void)testCommitValuesBackToTheFormObjectFromCellKeyPaths { + RKMappableObject *mappableObject = [[RKMappableObject new] autorelease]; + RKForm *form = [RKForm formForObject:mappableObject usingBlock:^(RKForm *form) { + [form addRowMappingAttribute:@"stringTest" toKeyPath:@"someTextProperty" onCellWithClass:[RKFormSpecTableViewCell class]]; + }]; + + RKTableItem *tableItem = [form.tableItems lastObject]; + RKFormSpecTableViewCell *cell = [[RKFormSpecTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; + cell.someTextProperty = @"testing 123"; + id mockTableController = [OCMockObject niceMockForClass:[RKTableController class]]; + [[[mockTableController expect] andReturn:cell] cellForObject:tableItem]; + [form didLoadInTableController:mockTableController]; + + // Create a cell + // Create a fake table view model + // stub out returning the cell from the table view model + + [form commitValuesToObject]; + assertThat(mappableObject.stringTest, is(equalTo(@"testing 123"))); +} + +- (void)testMakeTheTableItemPassKVCInvocationsThroughToTheUnderlyingMappedControlKeyPath { + // TODO: Implement me + // add a control + // invoke valueForKey: with the control value keyPath on the table item... +} + +- (void)testInvokeValueForKeyPathOnTheControlIfControlValueReturnsNil { + // TODO: Implement me + // add a custom control to the form + // the control value should return nil so that valueForKeyPath is invoked directly +} + +@end diff --git a/Tests/Application/UI/RKTableControllerTest.m b/Tests/Application/UI/RKTableControllerTest.m new file mode 100644 index 0000000000..0378a792bb --- /dev/null +++ b/Tests/Application/UI/RKTableControllerTest.m @@ -0,0 +1,2249 @@ +// +// RKTableControllerTest.m +// RestKit +// +// Created by Blake Watters on 8/3/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKTableController.h" +#import "RKTableSection.h" +#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 RKTableControllerTestTableViewController : UITableViewController +@end + +@implementation RKTableControllerTestTableViewController +@end + +@interface RKTableControllerTestViewController : UIViewController +@end + +@implementation RKTableControllerTestViewController +@end + +@interface RKTestUserTableViewCell : UITableViewCell +@end + +@implementation RKTestUserTableViewCell +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface RKTableControllerTest : RKTestCase + +@end + +@implementation RKTableControllerTest + +- (void)setUp +{ + [RKTestFactory setUp]; +} + +- (void)tearDown +{ + [RKTestFactory tearDown]; +} + +- (void)testInitializeWithATableViewController { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + assertThat(viewController.tableView, is(notNilValue())); + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThat(tableController.viewController, is(equalTo(viewController))); + assertThat(tableController.tableView, is(equalTo(viewController.tableView))); +} + +- (void)testInitializeWithATableViewAndViewController { + UITableView* tableView = [UITableView 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 { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + assertThat(viewController.tableView, is(notNilValue())); + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); +} + +- (void)testDisconnectFromTheTableViewOnDealloc { + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); + [pool drain]; + assertThat(viewController.tableView.delegate, is(nilValue())); + assertThat(viewController.tableView.dataSource, is(nilValue())); +} + +- (void)testNotDisconnectFromTheTableViewIfDelegateOrDataSourceAreNotSelf { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [[RKTableController alloc] initWithTableView:viewController.tableView viewController:viewController]; + viewController.tableView.delegate = viewController; + viewController.tableView.dataSource = viewController; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); + [tableController release]; + assertThat(viewController.tableView.delegate, isNot(nilValue())); + assertThat(viewController.tableView.dataSource, isNot(nilValue())); +} + +#pragma mark - Section Management + +- (void)testAddASection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThatInt([tableController.sections count], is(equalToInt(2))); +} + +- (void)testConnectTheSectionToTheTableModelOnAdd { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThat(section.tableController, is(equalTo(tableController))); +} + +- (void)testConnectTheSectionToTheCellMappingsOfTheTableModelWhenNil { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + assertThat(section.cellMappings, is(nilValue())); + [tableController addSection:section]; + assertThat(section.cellMappings, is(equalTo(tableController.cellMappings))); +} + +- (void)testNotConnectTheSectionToTheCellMappingsOfTheTableModelWhenNonNil { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + section.cellMappings = [NSMutableDictionary dictionary]; + [tableController addSection:section]; + assertThatBool(section.cellMappings == tableController.cellMappings, is(equalToBool(NO))); +} + +- (void)testCountTheSections { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThatInt(tableController.sectionCount, is(equalToInt(2))); +} + +- (void)testRemoveASection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThatInt(tableController.sectionCount, is(equalToInt(2))); + [tableController removeSection:section]; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); +} + +- (void)testNotLetRemoveTheLastSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThatInt(tableController.sectionCount, is(equalToInt(2))); + [tableController removeSection:section]; +} + +- (void)testInsertASectionAtATestificIndex { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* referenceSection = [RKTableSection section]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController insertSection:referenceSection atIndex:2]; + assertThatInt(tableController.sectionCount, is(equalToInt(6))); + assertThat([tableController.sections objectAtIndex:2], is(equalTo(referenceSection))); +} + +- (void)testRemoveASectionByIndex { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThatInt(tableController.sectionCount, is(equalToInt(2))); + [tableController removeSectionAtIndex:1]; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); +} + +- (void)testRaiseAnExceptionWhenAttemptingToRemoveTheLastSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + NSException* exception = nil; + @try { + [tableController removeSectionAtIndex:0]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, isNot(nilValue())); + } +} + +- (void)testReturnTheSectionAtAGivenIndex { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* referenceSection = [RKTableSection section]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController insertSection:referenceSection atIndex:2]; + assertThatInt(tableController.sectionCount, is(equalToInt(6))); + assertThat([tableController sectionAtIndex:2], is(equalTo(referenceSection))); +} + +- (void)testRemoveAllSections { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + assertThatInt(tableController.sectionCount, is(equalToInt(5))); + [tableController removeAllSections]; + assertThatInt(tableController.sectionCount, is(equalToInt(1))); +} + +- (void)testReturnASectionByHeaderTitle { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addSection:[RKTableSection section]]; + [tableController addSection:[RKTableSection section]]; + RKTableSection* titledSection = [RKTableSection section]; + titledSection.headerTitle = @"Testing"; + [tableController addSection:titledSection]; + [tableController addSection:[RKTableSection section]]; + assertThat([tableController sectionWithHeaderTitle:@"Testing"], is(equalTo(titledSection))); +} + +- (void)testNotifyTheTableViewOnSectionInsertion { + 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]; + [tableController addSection:[RKTableSection section]]; + [mockTableView verify]; +} + +- (void)testNotifyTheTableViewOnSectionRemoval { + 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]; + [[mockTableView expect] deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; + RKTableSection *section = [RKTableSection section]; + [tableController addSection:section]; + [tableController removeSection:section]; + [mockTableView verify]; +} + +- (void)testNotifyTheTableOfSectionRemovalAndReaddWhenRemovingAllSections { + 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]; + [[mockTableView expect] deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:tableController.defaultRowAnimation]; + [[mockTableView expect] insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:tableController.defaultRowAnimation]; + RKTableSection *section = [RKTableSection section]; + [tableController addSection:section]; + [tableController removeAllSections]; + [mockTableView verify]; +} + +#pragma mark - UITableViewDataSource Tests + +- (void)testRaiseAnExceptionIfSentAMessageWithATableViewItIsNotBoundTo { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + NSException* exception = nil; + @try { + [tableController numberOfSectionsInTableView:[UITableView new]]; + } + @catch (NSException* e) { + exception = e; + } + @finally { + assertThat(exception, is(notNilValue())); + } +} + +- (void)testReturnTheNumberOfSectionsInTableView { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatInt([tableController numberOfSectionsInTableView:viewController.tableView], is(equalToInt(1))); + [tableController addSection:[RKTableSection section]]; + assertThatInt([tableController numberOfSectionsInTableView:viewController.tableView], is(equalToInt(2))); +} + +- (void)testReturnTheNumberOfRowsInSectionInTableView { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatInt([tableController tableView:viewController.tableView numberOfRowsInSection:0], is(equalToInt(0))); + NSArray* objects = [NSArray arrayWithObject:@"one"]; + [tableController loadObjects:objects]; + assertThatInt([tableController tableView:viewController.tableView numberOfRowsInSection:0], is(equalToInt(1))); +} + +- (void)testReturnTheHeaderTitleForSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThat([tableController tableView:viewController.tableView titleForHeaderInSection:1], is(nilValue())); + section.headerTitle = @"RestKit!"; + assertThat([tableController tableView:viewController.tableView titleForHeaderInSection:1], is(equalTo(@"RestKit!"))); +} + +- (void)testReturnTheTitleForFooterInSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + [tableController addSection:section]; + assertThat([tableController tableView:viewController.tableView titleForFooterInSection:1], is(nilValue())); + section.footerTitle = @"RestKit!"; + assertThat([tableController tableView:viewController.tableView titleForFooterInSection:1], is(equalTo(@"RestKit!"))); +} + +- (void)testReturnTheNumberOfRowsAcrossAllSections { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableSection* section = [RKTableSection section]; + id sectionMock = [OCMockObject partialMockForObject:section]; + NSUInteger rowCount = 5; + [[[sectionMock stub] andReturnValue:OCMOCK_VALUE(rowCount)] rowCount]; + [tableController addSection:section]; + assertThatInt(tableController.rowCount, is(equalToInt(5))); +} + +- (void)testReturnTheTableViewCellForRowAtIndexPath { + 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) { + // Detail text label won't appear with default style... + cellMapping.style = UITableViewCellStyleValue1; + [cellMapping addDefaultMappings]; + }]]; + UITableViewCell* cell = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThat(cell.textLabel.text, is(equalTo(@"Test!"))); + assertThat(cell.detailTextLabel.text, is(equalTo(@"Details!"))); + +} + +#pragma mark - Table Cell Mapping + +- (void)testInitializeCellMappings { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThat(tableController.cellMappings, is(notNilValue())); +} + +- (void)testRegisterMappingsForObjectsToTableViewCell { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:[RKTableViewCellMapping cellMapping]]; + RKObjectMapping* mapping = [tableController.cellMappings cellMappingForClass:[RKTestUser class]]; + assertThat(mapping, isNot(nilValue())); + assertThatBool([mapping.objectClass isSubclassOfClass:[UITableViewCell class]], is(equalToBool(YES))); +} + +- (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClass { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping cellMapping]; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:cellMapping]; + assertThat(cellMapping.reuseIdentifier, is(equalTo(@"UITableViewCell"))); +} + +- (void)testDefaultTheReuseIdentifierToTheNameOfTheObjectClassWhenCreatingMappingWithBlockSyntax { + RKTableControllerTestTableViewController *viewController = [RKTableControllerTestTableViewController new]; + RKTableController *tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThat([tableController.cellMappings cellMappingForClass:[RKTestUser class]], is(nilValue())); + RKTableViewCellMapping *cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping *cellMapping) { + cellMapping.cellClass = [RKTestUserTableViewCell class]; + }]; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:cellMapping]; + assertThat(cellMapping.reuseIdentifier, is(equalTo(@"RKTestUserTableViewCell"))); +} + +- (void)testReturnTheObjectForARowAtIndexPath { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTestUser* user = [RKTestUser user]; + [tableController loadObjects:[NSArray arrayWithObject:user]]; + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; + assertThatBool(user == [tableController objectForRowAtIndexPath:indexPath], is(equalToBool(YES))); +} + +- (void)testReturnTheCellMappingForTheRowAtIndexPath { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping cellMapping]; + [tableController mapObjectsWithClass:[RKTestUser class] toTableCellsWithMapping:cellMapping]; + [tableController loadObjects:[NSArray arrayWithObject:[RKTestUser user]]]; + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; + assertThat([tableController cellMappingForObjectAtIndexPath:indexPath], is(equalTo(cellMapping))); +} + +- (void)testReturnATableViewCellForTheObjectAtAGivenIndexPath { + RKMappableObject* object = [RKMappableObject new]; + object.stringTest = @"Testing!!"; + RKTableViewCellMapping* cellMapping = [RKTableViewCellMapping mappingForClass:[UITableViewCell class]]; + [cellMapping mapKeyPath:@"stringTest" toAttribute:@"textLabel.text"]; + NSArray* objects = [NSArray arrayWithObject:object]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + [mappings setCellMapping:cellMapping forClass:[RKMappableObject class]]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + UITableViewController* tableViewController = [UITableViewController new]; + tableViewController.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0,0,0,0) style:UITableViewStylePlain]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:tableViewController]; + [tableController insertSection:section atIndex:0]; + tableController.cellMappings = mappings; + + UITableViewCell* cell = [tableController cellForObjectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThat(cell, isNot(nilValue())); + assertThat(cell.textLabel.text, is(equalTo(@"Testing!!"))); +} + +- (void)testChangeTheReuseIdentifierWhenMutatedWithinTheBlockInitializer { + 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) { + cellMapping.cellClass = [RKTestUserTableViewCell class]; + cellMapping.reuseIdentifier = @"RKTestUserOverride"; + }]]; + RKTableViewCellMapping* userCellMapping = [tableController.cellMappings cellMappingForClass:[RKTestUser class]]; + assertThat(userCellMapping, isNot(nilValue())); + assertThat(userCellMapping.reuseIdentifier, is(equalTo(@"RKTestUserOverride"))); +} + +#pragma mark - Static Object Loading + +- (void)testLoadAnArrayOfObjects { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + NSArray* objects = [NSArray arrayWithObject:@"one"]; + assertThat([tableController sectionAtIndex:0].objects, is(empty())); + [tableController loadObjects:objects]; + assertThat([tableController sectionAtIndex:0].objects, is(equalTo(objects))); +} + +- (void)testLoadAnArrayOfObjectsToTheTestifiedSection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addSection:[RKTableSection section]]; + NSArray* objects = [NSArray arrayWithObject:@"one"]; + assertThat([tableController sectionAtIndex:1].objects, is(empty())); + [tableController loadObjects:objects inSection:1]; + assertThat([tableController sectionAtIndex:1].objects, is(equalTo(objects))); +} + +- (void)testLoadAnArrayOfTableItems { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + NSArray* tableItems = [RKTableItem tableItemsFromStrings:@"One", @"Two", @"Three", nil]; + [tableController loadTableItems:tableItems]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"One"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"Two"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"Three"))); +} + +- (void)testAllowYouToTriggerAnEmptyLoad { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +#pragma mark - Network Load + +- (void)testLoadCollectionOfObjectsAndMapThemIntoTableViewCells { + 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"]; + }]]; + 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.rowCount, is(equalToInt(3))); +} + +- (void)testSetTheModelToTheLoadedStateIfObjectsAreLoadedSuccessfully { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + NSArray* objects = [NSArray arrayWithObject:[RKTestUser new]]; + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController objectLoader:mockLoader didLoadObjects:objects]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); +} + +- (void)testSetTheModelToErrorStateIfTheObjectLoaderFailsWithAnError { + 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))); +} + +- (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 { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + NSArray* objects = [NSArray array]; + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController objectLoader:mockLoader didLoadObjects:objects]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testSetTheModelToALoadedStateEvenIfTheObjectLoaderReturnsAnEmptyCollection { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + NSArray* objects = [NSArray array]; + id mockLoader = [OCMockObject mockForClass:[RKObjectLoader class]]; + [tableController objectLoader:mockLoader didLoadObjects:objects]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + assertThatBool([tableController isLoaded], is(equalToBool(YES))); +} + +- (void)testEnterTheLoadingStateWhenTheRequestStartsLoading { + 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))); +} + +- (void)testExitTheLoadingStateWhenTheRequestFinishesLoading { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + assertThatBool([tableController isLoaded], is(equalToBool(NO))); + assertThatBool([tableController isLoading], is(equalToBool(NO))); + id mockLoader = [OCMockObject niceMockForClass:[RKObjectLoader class]]; + [tableController requestDidStartLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(YES))); + [tableController objectLoaderDidFinishLoading:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); +} + +- (void)testClearTheLoadingStateWhenARequestIsCancelled { + 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))); + [tableController requestDidCancelLoad:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); +} + +- (void)testClearTheLoadingStateWhenARequestTimesOut { + 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))); + [tableController requestDidTimeout:mockLoader]; + assertThatBool([tableController isLoading], is(equalToBool(NO))); +} + +- (void)testDoSomethingWhenTheRequestLoadsAnUnexpectedResponse { + RKLogCritical(@"PENDING - Undefined Behavior!!!"); +} + +- (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 = [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 mockDelegate = [OCMockObject partialMockForObject:[RKTableControllerTestDelegate new]]; + [[[mockDelegate expect] andForwardToRealObject] tableControllerDidStartLoad: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)testNotifyTheDelegateWhenLoadingFinishes { + 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] andForwardToRealObject] tableControllerDidFinishLoad: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)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 = [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] andForwardToRealObject] tableController:tableController didFailLoadWithError:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/fail" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenAnEmptyCollectionIsLoaded { + 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"]; + }]]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + delegate.timeout = 5; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableControllerDidBecomeEmpty:tableController]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenModelWillLoadWithObjectLoader { + 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] andForwardToRealObject] tableController:tableController willLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenModelDidLoadWithObjectLoader { + 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] andForwardToRealObject] tableController:tableController didLoadTableWithObjectLoader:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [mockDelegate waitForLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenModelDidCancelLoad { + 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] andForwardToRealObject] tableControllerDidCancelLoad:tableController]; + tableController.delegate = mockDelegate; + [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [tableController cancelLoad]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenDidEndEditingARow { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didEndEditing:OCMOCK_ANY + atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + tableController.delegate = mockDelegate; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + [tableController tableView:tableController.tableView didEndEditingRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenWillBeginEditingARow { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + willBeginEditing:OCMOCK_ANY + atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + tableController.delegate = mockDelegate; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + [tableController tableView:tableController.tableView willBeginEditingRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenAnObjectIsInserted { + NSArray* objects = [NSArray arrayWithObject:@"first object"]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"first object" + atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"new object" + atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + tableController.delegate = mockDelegate; + [tableController loadObjects:objects]; + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"first object"))); + [[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"))); + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenAnObjectIsUpdated { + NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"first object" + atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"second object" + atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didUpdateObject:@"new second object" + atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + tableController.delegate = mockDelegate; + [tableController loadObjects:objects]; + assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:0], is(equalTo(@"first object"))); + 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"))); + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testNotifyTheDelegateWhenAnObjectIsDeleted { + NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"first object" + atIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didInsertObject:@"second object" + atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + didDeleteObject:@"second object" + atIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + tableController.delegate = mockDelegate; + [tableController loadObjects:objects]; + assertThat([[tableController.sections objectAtIndex:0] objectAtIndex:0], is(equalTo(@"first object"))); + 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"))); + 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 = [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:RKTableControllerDidStartLoadNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidStartLoadNotification 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)testPostANotificationWhenLoadingFinishes { + 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:RKTableControllerDidFinishLoadNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidFinishLoadNotification 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)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 = [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:RKTableControllerDidLoadErrorNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadErrorNotification object:tableController userInfo:OCMOCK_ANY]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + tableController.delegate = delegate; + + [tableController loadTableFromResourcePath:@"/fail" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + [observerMock verify]; +} + +- (void)testPostANotificationWhenAnEmptyCollectionIsLoaded { + 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"]; + }]]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidLoadEmptyNotification object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidLoadEmptyNotification object:tableController]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + tableController.delegate = delegate; + [tableController loadTableFromResourcePath:@"/empty/array" usingBlock:^(RKObjectLoader* objectLoader) { + objectLoader.objectMapping = [RKObjectMapping mappingForClass:[RKTestUser class] usingBlock:^(RKObjectMapping* mapping) { + [mapping mapAttributes:@"name", nil]; + }]; + }]; + [delegate waitForLoad]; + [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 { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + [tableController showImageInOverlay:image]; + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); +} + +- (void)testPermitYouToRemoveAnImageOverlayFromTheTable { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + [tableController showImageInOverlay:image]; + assertThat([tableController.tableView.superview subviews], isNot(empty())); + [tableController removeImageOverlay]; + assertThat([tableController.tableView.superview subviews], is(nilValue())); +} + +- (void)testTriggerDisplayOfTheErrorViewOnTransitionToErrorState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + tableController.imageForError = image; + id mockError = [OCMockObject mockForClass:[NSError class]]; + [tableController objectLoader:nil didFailWithError:mockError]; + assertThatBool([tableController isError], is(equalToBool(YES))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); +} + +- (void)testTriggerHidingOfTheErrorViewOnTransitionOutOfTheErrorState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + tableController.imageForError = image; + id mockError = [OCMockObject niceMockForClass:[NSError class]]; + [tableController objectLoader:nil didFailWithError:mockError]; + assertThatBool([tableController isError], is(equalToBool(YES))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); + [tableController loadTableItems:[NSArray arrayWithObject:[RKTableItem tableItem]]]; + assertThat(tableController.error, is(nilValue())); + assertThat(tableController.stateOverlayImageView.image, is(nilValue())); +} + +- (void)testTriggerDisplayOfTheEmptyViewOnTransitionToEmptyState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + tableController.imageForEmpty = image; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); +} + +- (void)testTriggerHidingOfTheEmptyViewOnTransitionOutOfTheEmptyState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + tableController.imageForEmpty = image; + [tableController loadEmpty]; + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); + [tableController loadTableItems:[NSArray arrayWithObject:[RKTableItem tableItem]]]; + assertThat(tableController.stateOverlayImageView.image, is(nilValue())); +} + +- (void)testTriggerDisplayOfTheLoadingViewOnTransitionToTheLoadingState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + tableController.loadingView = spinner; + [tableController setValue:[NSNumber numberWithBool:YES] forKey:@"loading"]; + UIView* view = [tableController.tableOverlayView.subviews lastObject]; + assertThatBool(view == spinner, is(equalToBool(YES))); +} + +- (void)testTriggerHidingOfTheLoadingViewOnTransitionOutOfTheLoadingState { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + tableController.loadingView = spinner; + [tableController setValue:[NSNumber numberWithBool:YES] forKey:@"loading"]; + UIView* loadingView = [tableController.tableOverlayView.subviews lastObject]; + assertThatBool(loadingView == spinner, is(equalToBool(YES))); + [tableController setValue:[NSNumber numberWithBool:NO] forKey:@"loading"]; + loadingView = [tableController.tableOverlayView.subviews lastObject]; + assertThat(loadingView, is(nilValue())); +} + +#pragma mark - Header, Footer, and Empty Rows + +- (void)testShowHeaderRows { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + NSArray* tableItems = [RKTableItem tableItemsFromStrings:@"One", @"Two", @"Three", nil]; + [tableController loadTableItems:tableItems]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(4))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + UITableViewCell* cellFour = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Header"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"One"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"Two"))); + assertThat(cellFour.textLabel.text, is(equalTo(@"Three"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellTwo forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellThree forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellFour forRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(NO))); + assertThatBool(cellTwo.hidden, is(equalToBool(NO))); + assertThatBool(cellThree.hidden, is(equalToBool(NO))); + assertThatBool(cellFour.hidden, is(equalToBool(NO))); +} + +- (void)testShowFooterRows { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + NSArray* tableItems = [RKTableItem tableItemsFromStrings:@"One", @"Two", @"Three", nil]; + [tableController loadTableItems:tableItems]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(4))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + UITableViewCell* cellFour = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"One"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"Two"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"Three"))); + assertThat(cellFour.textLabel.text, is(equalTo(@"Footer"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellTwo forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellThree forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellFour forRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(NO))); + assertThatBool(cellTwo.hidden, is(equalToBool(NO))); + assertThatBool(cellThree.hidden, is(equalToBool(NO))); + assertThatBool(cellFour.hidden, is(equalToBool(NO))); +} + +- (void)testHideHeaderRowsWhenEmptyWhenPropertyIsNotSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsHeaderRowsWhenEmpty = NO; + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(1))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Header"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(YES))); +} + +- (void)testHideFooterRowsWhenEmptyWhenPropertyIsNotSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsFooterRowsWhenEmpty = NO; + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(1))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Footer"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(YES))); +} + +- (void)testRemoveHeaderAndFooterCountsWhenDeterminingIsEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsFooterRowsWhenEmpty = NO; + tableController.showsHeaderRowsWhenEmpty = NO; + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); +} + +- (void)testNotShowTheEmptyItemWhenTheTableIsNotEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + NSArray* tableItems = [RKTableItem tableItemsFromStrings:@"One", @"Two", @"Three", nil]; + [tableController loadTableItems:tableItems]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(6))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + UITableViewCell* cellFour = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + UITableViewCell* cellFive = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:4 inSection:0]]; + UITableViewCell* cellSix = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:5 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Empty"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"Header"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"One"))); + assertThat(cellFour.textLabel.text, is(equalTo(@"Two"))); + assertThat(cellFive.textLabel.text, is(equalTo(@"Three"))); + assertThat(cellSix.textLabel.text, is(equalTo(@"Footer"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellTwo forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellThree forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellFour forRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellFive forRowAtIndexPath:[NSIndexPath indexPathForRow:4 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellSix forRowAtIndexPath:[NSIndexPath indexPathForRow:5 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(YES))); + assertThatBool(cellTwo.hidden, is(equalToBool(NO))); + assertThatBool(cellThree.hidden, is(equalToBool(NO))); + assertThatBool(cellFour.hidden, is(equalToBool(NO))); + assertThatBool(cellFive.hidden, is(equalToBool(NO))); + assertThatBool(cellSix.hidden, is(equalToBool(NO))); +} + +- (void)testShowTheEmptyItemWhenTheTableIsEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsHeaderRowsWhenEmpty = NO; + tableController.showsFooterRowsWhenEmpty = NO; + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Empty"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"Header"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"Footer"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellTwo forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellThree forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(NO))); + assertThatBool(cellTwo.hidden, is(equalToBool(YES))); + assertThatBool(cellThree.hidden, is(equalToBool(YES))); +} + +- (void)testShowTheEmptyItemPlusHeadersAndFootersWhenTheTableIsEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController addHeaderRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Header"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController addFooterRowForItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Footer"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + [tableController setEmptyItem:[RKTableItem tableItemUsingBlock:^(RKTableItem* tableItem) { + tableItem.text = @"Empty"; + tableItem.cellMapping = [RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + [cellMapping addDefaultMappings]; + }]; + }]]; + tableController.showsHeaderRowsWhenEmpty = YES; + tableController.showsFooterRowsWhenEmpty = YES; + [tableController loadEmpty]; + assertThatBool([tableController isLoaded], is(equalToBool(YES))); + assertThatInt(tableController.rowCount, is(equalToInt(3))); + assertThatBool([tableController isEmpty], is(equalToBool(YES))); + UITableViewCell* cellOne = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + UITableViewCell* cellTwo = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + UITableViewCell* cellThree = [tableController tableView:tableController.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThat(cellOne.textLabel.text, is(equalTo(@"Empty"))); + assertThat(cellTwo.textLabel.text, is(equalTo(@"Header"))); + assertThat(cellThree.textLabel.text, is(equalTo(@"Footer"))); + [tableController tableView:tableController.tableView willDisplayCell:cellOne forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellTwo forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + [tableController tableView:tableController.tableView willDisplayCell:cellThree forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThatBool(cellOne.hidden, is(equalToBool(NO))); + assertThatBool(cellTwo.hidden, is(equalToBool(NO))); + assertThatBool(cellThree.hidden, is(equalToBool(NO))); +} + +#pragma mark - UITableViewDelegate Tests + +- (void)testInvokeTheOnSelectCellForObjectAtIndexPathBlockHandler { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + __block BOOL dispatched = NO; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + cellMapping.onSelectCellForObjectAtIndexPath = ^(UITableViewCell* cell, id object, NSIndexPath* indexPath) { + dispatched = YES; + }; + }]]; + [tableController tableView:tableController.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(dispatched, is(equalToBool(YES))); +} + +- (void)testInvokeTheOnCellWillAppearForObjectAtIndexPathBlockHandler { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + __block BOOL dispatched = NO; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem] withMapping:[RKTableViewCellMapping cellMappingUsingBlock:^(RKTableViewCellMapping* cellMapping) { + cellMapping.onCellWillAppearForObjectAtIndexPath = ^(UITableViewCell* cell, id object, NSIndexPath* indexPath) { + dispatched = YES; + }; + }]]; + id mockCell = [OCMockObject niceMockForClass:[UITableViewCell class]]; + [tableController tableView:tableController.tableView willDisplayCell:mockCell forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(dispatched, is(equalToBool(YES))); +} + +- (void)testOptionallyHideHeaderRowsWhenTheyAppearAndTheTableIsEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.showsHeaderRowsWhenEmpty = NO; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController addHeaderRowForItem:tableItem]; + [tableController loadEmpty]; + id mockCell = [OCMockObject niceMockForClass:[UITableViewCell class]]; + [[mockCell expect] setHidden:YES]; + [tableController tableView:tableController.tableView willDisplayCell:mockCell forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [mockCell verify]; +} + +- (void)testOptionallyHideFooterRowsWhenTheyAppearAndTheTableIsEmpty { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.showsFooterRowsWhenEmpty = NO; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController addFooterRowForItem:tableItem]; + [tableController loadEmpty]; + id mockCell = [OCMockObject niceMockForClass:[UITableViewCell class]]; + [[mockCell expect] setHidden:YES]; + [tableController tableView:tableController.tableView willDisplayCell:mockCell forRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [mockCell verify]; +} + +- (void)testInvokeABlockCallbackWhenTheCellAccessoryButtonIsTapped { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + __block BOOL dispatched = NO; + 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 { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + RKTableItem* tableItem = [RKTableItem tableItem]; + NSString* deleteTitle = @"Delete Me"; + 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 { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canEditRows = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + 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 { + 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] 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))); +} + +#pragma mark Variable Height Rows + +- (void)testReturnTheRowHeightConfiguredOnTheTableViewWhenVariableHeightRowsIsDisabled { + 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] 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 { + 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] 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 { + 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] 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))); +} + +#pragma mark - Editing + +- (void)testAllowEditingWhenTheCanEditRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canEditRows = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + BOOL delegateCanEdit = [tableController tableView:tableController.tableView + canEditRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); +} + +- (void)testCommitADeletionWhenTheCanEditRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canEditRows = YES; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + BOOL delegateCanEdit = [tableController tableView:tableController.tableView + canEditRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + assertThatInt([tableController rowCount], is(equalToInt(1))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"First Object"))); +} + +- (void)testNotCommitADeletionWhenTheCanEditRowsPropertyIsNotSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + BOOL delegateCanEdit = [tableController tableView:tableController.tableView + canEditRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanEdit, is(equalToBool(NO))); + [tableController tableView:tableController.tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"First Object"))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], + is(equalTo(@"Second Object"))); +} + +- (void)testDoNothingToCommitAnInsertionWhenTheCanEditRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canEditRows = YES; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + BOOL delegateCanEdit = [tableController tableView:tableController.tableView + canEditRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanEdit, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + commitEditingStyle:UITableViewCellEditingStyleInsert + forRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"First Object"))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], + is(equalTo(@"Second Object"))); +} + +- (void)testAllowMovingWhenTheCanMoveRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canMoveRows = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + BOOL delegateCanMove = [tableController tableView:tableController.tableView + canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanMove, is(equalToBool(YES))); +} + +- (void)testMoveARowWithinASectionWhenTheCanMoveRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canMoveRows = YES; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + BOOL delegateCanMove = [tableController tableView:tableController.tableView + 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]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], + is(equalTo(@"First Object"))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"Second Object"))); +} + +- (void)testMoveARowAcrossSectionsWhenTheCanMoveRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canMoveRows = YES; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThatInt([tableController sectionCount], is(equalToInt(1))); + BOOL delegateCanMove = [tableController tableView:tableController.tableView + canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanMove, is(equalToBool(YES))); + [tableController tableView:tableController.tableView + moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + toIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThatInt([tableController sectionCount], is(equalToInt(2))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]], + is(equalTo(@"First Object"))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"Second Object"))); +} + +- (void)testNotMoveARowWhenTheCanMoveRowsPropertyIsNotSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + [tableController loadObjects:[NSArray arrayWithObjects:@"First Object", @"Second Object", nil]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + BOOL delegateCanMove = [tableController tableView:tableController.tableView + canMoveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanMove, is(equalToBool(NO))); + [tableController tableView:tableController.tableView + moveRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + toIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; + assertThatInt([tableController rowCount], is(equalToInt(2))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]], + is(equalTo(@"First Object"))); + assertThat([tableController objectForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]], + is(equalTo(@"Second Object"))); +} + +#pragma mark - Reachability Integration + +- (void)testTransitionToTheOnlineStateWhenAReachabilityNoticeIsReceived { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL online = YES; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:objectManager]; + assertThatBool(tableController.isOnline, is(equalToBool(YES))); +} + +- (void)testTransitionToTheOfflineStateWhenAReachabilityNoticeIsReceived { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL online = NO; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.objectManager = objectManager; + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; + assertThatBool(tableController.isOnline, is(equalToBool(NO))); +} + +- (void)testNotifyTheDelegateOnTransitionToOffline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + [mockManager setExpectationOrderMatters:YES]; + RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(networkStatus)] networkStatus]; + BOOL online = YES; // Initial online state for table + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = NO; // After the notification is posted + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + 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]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testPostANotificationOnTransitionToOffline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + [mockManager setExpectationOrderMatters:YES]; + RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(networkStatus)] networkStatus]; + BOOL online = YES; // Initial online state for table + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = NO; // After the notification is posted + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + + id observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidBecomeOffline object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidBecomeOffline object:tableController]; + + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; + [observerMock verify]; +} + +- (void)testNotifyTheDelegateOnTransitionToOnline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL online = NO; + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = YES; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + 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]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testPostANotificationOnTransitionToOnline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + BOOL online = NO; + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = YES; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + 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 observerMock = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:observerMock name:RKTableControllerDidBecomeOnline object:tableController]; + [[observerMock expect] notificationWithName:RKTableControllerDidBecomeOnline object:tableController]; + + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:objectManager]; + [observerMock verify]; +} + +- (void)testShowTheOfflineImageOnTransitionToOffline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + [mockManager setExpectationOrderMatters:YES]; + RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(networkStatus)] networkStatus]; + BOOL online = YES; // Initial online state for table + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = NO; // After the notification is posted + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + UIImage* image = [RKTestFixture imageWithContentsOfFixture:@"blake.png"]; + tableController.imageForOffline = image; + + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; + assertThatBool(tableController.isOnline, is(equalToBool(NO))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); +} + +- (void)testRemoveTheOfflineImageOnTransitionToOnlineFromOffline { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + id mockManager = [OCMockObject partialMockForObject:objectManager]; + [mockManager setExpectationOrderMatters:YES]; + RKObjectManagerNetworkStatus networkStatus = RKObjectManagerNetworkStatusOnline; + [[[mockManager stub] andReturnValue:OCMOCK_VALUE(networkStatus)] networkStatus]; + BOOL online = YES; // Initial online state for table + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + online = NO; // After the notification is posted + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + 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; + + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOfflineNotification object:objectManager]; + assertThatBool(tableController.isOnline, is(equalToBool(NO))); + UIImageView* imageView = tableController.stateOverlayImageView; + assertThat(imageView, isNot(nilValue())); + assertThat(imageView.image, is(equalTo(image))); + + online = YES; + [[[mockManager expect] andReturnValue:OCMOCK_VALUE(online)] isOnline]; + [[NSNotificationCenter defaultCenter] postNotificationName:RKObjectManagerDidBecomeOnlineNotification object:objectManager]; + assertThatBool(tableController.isOnline, is(equalToBool(YES))); + imageView = tableController.stateOverlayImageView; + assertThat(imageView.image, is(nilValue())); +} + +#pragma mark - Swipe Menus + +- (void)testAllowSwipeMenusWhenTheSwipeViewsEnabledPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.cellSwipeViewsEnabled = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + assertThatBool(tableController.canEditRows, is(equalToBool(NO))); + assertThatBool(tableController.cellSwipeViewsEnabled, is(equalToBool(YES))); +} + +- (void)testNotAllowEditingWhenTheSwipeViewsEnabledPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.cellSwipeViewsEnabled = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + BOOL delegateCanEdit = [tableController tableView:tableController.tableView + canEditRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + assertThatBool(delegateCanEdit, is(equalToBool(NO))); +} + +- (void)testRaiseAnExceptionWhenEnablingSwipeViewsWhenTheCanEditRowsPropertyIsSet { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.canEditRows = YES; + + NSException* exception = nil; + @try { + tableController.cellSwipeViewsEnabled = YES; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, isNot(nilValue())); + } +} + +- (void)testCallTheDelegateBeforeShowingTheSwipeView { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.cellSwipeViewsEnabled = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + willAddSwipeView:OCMOCK_ANY + toCell:OCMOCK_ANY + forObject:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + [tableController addSwipeViewTo:[RKTestUserTableViewCell new] + withObject:@"object" + direction:UISwipeGestureRecognizerDirectionRight]; + STAssertNoThrow([mockDelegate verify], nil); +} + +- (void)testCallTheDelegateBeforeHidingTheSwipeView { + RKTableControllerTestTableViewController* viewController = [RKTableControllerTestTableViewController new]; + RKTableController* tableController = [RKTableController tableControllerForTableViewController:viewController]; + tableController.cellSwipeViewsEnabled = YES; + RKTableItem* tableItem = [RKTableItem tableItem]; + RKTableControllerTestDelegate* delegate = [RKTableControllerTestDelegate new]; + id mockDelegate = [OCMockObject partialMockForObject:delegate]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + willAddSwipeView:OCMOCK_ANY + toCell:OCMOCK_ANY + forObject:OCMOCK_ANY]; + [[[mockDelegate expect] andForwardToRealObject] tableController:tableController + willRemoveSwipeView:OCMOCK_ANY + fromCell:OCMOCK_ANY + forObject:OCMOCK_ANY]; + tableController.delegate = mockDelegate; + [tableController loadTableItems:[NSArray arrayWithObject:tableItem]]; + [tableController addSwipeViewTo:[RKTestUserTableViewCell new] + withObject:@"object" + direction:UISwipeGestureRecognizerDirectionRight]; + [tableController animationDidStopAddingSwipeView:nil + finished:nil + context:nil]; + [tableController removeSwipeView:YES]; + STAssertNoThrow([mockDelegate verify], nil); +} + +@end diff --git a/Tests/Application/UI/RKTableViewCellMappingTest.m b/Tests/Application/UI/RKTableViewCellMappingTest.m new file mode 100644 index 0000000000..bc844d38fc --- /dev/null +++ b/Tests/Application/UI/RKTableViewCellMappingTest.m @@ -0,0 +1,34 @@ +// +// RKTableViewCellMappingTest.m +// RestKit +// +// Created by Blake Watters on 8/4/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKTableViewCellMapping.h" + +@interface RKTableViewCellMappingTest : RKTestCase + +@end + +@implementation RKTableViewCellMappingTest + +- (void)testCallTheDesignatedInitializerOnUITableViewCell { + +} + +- (void)testDequeueReusableCells { + +} + +- (void)testSetTheAccessoryType { + +} + +- (void)testSetTheCellStyle { + +} + +@end diff --git a/Tests/Application/UI/RKTableViewCellMappingsTest.m b/Tests/Application/UI/RKTableViewCellMappingsTest.m new file mode 100644 index 0000000000..407cf264c7 --- /dev/null +++ b/Tests/Application/UI/RKTableViewCellMappingsTest.m @@ -0,0 +1,67 @@ +// +// RKTableViewCellMappingsTest.m +// RestKit +// +// Created by Blake Watters on 8/9/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKTableViewCellMappings.h" +#import "RKTestUser.h" +#import "RKTestAddress.h" + +@interface RKTestSubclassedUser : RKTestUser +@end +@implementation RKTestSubclassedUser +@end + +@interface RKTableViewCellMappingsTest : RKTestCase + +@end + +@implementation RKTableViewCellMappingsTest + +- (void)testRaiseAnExceptionWhenAnAttemptIsMadeToRegisterAnExistingMappableClass { + RKTableViewCellMappings* cellMappings = [RKTableViewCellMappings cellMappings]; + RKTableViewCellMapping* firstMapping = [RKTableViewCellMapping cellMapping]; + RKTableViewCellMapping* secondMapping = [RKTableViewCellMapping cellMapping]; + [cellMappings setCellMapping:firstMapping forClass:[RKTestUser class]]; + NSException* exception = nil; + @try { + [cellMappings setCellMapping:secondMapping forClass:[RKTestUser class]]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, is(notNilValue())); + } +} + +- (void)testFindCellMappingsWithAnExactClassMatch { + RKTableViewCellMappings* cellMappings = [RKTableViewCellMappings cellMappings]; + RKTableViewCellMapping* firstMapping = [RKTableViewCellMapping cellMapping]; + RKTableViewCellMapping* secondMapping = [RKTableViewCellMapping cellMapping]; + [cellMappings setCellMapping:firstMapping forClass:[RKTestSubclassedUser class]]; + [cellMappings setCellMapping:secondMapping forClass:[RKTestUser class]]; + assertThat([cellMappings cellMappingForObject:[RKTestUser new]], is(equalTo(secondMapping))); +} + +- (void)testFindCellMappingsWithASubclassMatch { + RKTableViewCellMappings* cellMappings = [RKTableViewCellMappings cellMappings]; + RKTableViewCellMapping* firstMapping = [RKTableViewCellMapping cellMapping]; + RKTableViewCellMapping* secondMapping = [RKTableViewCellMapping cellMapping]; + [cellMappings setCellMapping:firstMapping forClass:[RKTestUser class]]; + [cellMappings setCellMapping:secondMapping forClass:[RKTestSubclassedUser class]]; + assertThat([cellMappings cellMappingForObject:[RKTestSubclassedUser new]], is(equalTo(secondMapping))); +} + +- (void)testReturnTheCellMappingForAnObjectInstance { + RKTableViewCellMappings* cellMappings = [RKTableViewCellMappings cellMappings]; + RKTableViewCellMapping* mapping = [RKTableViewCellMapping cellMapping]; + [cellMappings setCellMapping:mapping forClass:[RKTestUser class]]; + assertThat([cellMappings cellMappingForObject:[RKTestUser new]], is(equalTo(mapping))); +} + +@end diff --git a/Tests/Application/UI/RKTableViewSectionTest.m b/Tests/Application/UI/RKTableViewSectionTest.m new file mode 100644 index 0000000000..019b017636 --- /dev/null +++ b/Tests/Application/UI/RKTableViewSectionTest.m @@ -0,0 +1,99 @@ +// +// RKTableViewSectionTest.m +// RestKit +// +// Created by Blake Watters on 8/3/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKTableSection.h" +#import "RKTableViewCellMappings.h" +#import "RKTableViewCellMapping.h" +#import "RKTableController.h" + +@interface RKTableViewSectionTest : RKTestCase + +@end + +@implementation RKTableViewSectionTest + +- (void)testInitializeASection { + RKTableSection* section = [RKTableSection section]; + assertThat(section.objects, is(notNilValue())); + assertThat(section.objects, is(empty())); + assertThat(section.cellMappings, is(nilValue())); +} + +- (void)testInitializeASectionWithObjectsAndMappings { + NSArray* objects = [NSArray array]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat(section.objects, is(notNilValue())); + assertThat(section.cellMappings, isNot(nilValue())); + assertThat(section.objects, is(equalTo(objects))); + assertThat(section.cellMappings, is(equalTo(mappings))); +} + +- (void)testMakeAMutableCopyOfTheObjectsItIsInitializedWith { + NSArray* objects = [NSArray array]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat(section.objects, is(instanceOf([NSMutableArray class]))); +} + +- (void)testReturnTheNumberOfRowsInTheSection { + NSArray* objects = [NSArray arrayWithObject:@"first object"]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThatInt(section.rowCount, is(equalToInt(1))); +} + +- (void)testReturnTheObjectAtAGivenIndex { + NSArray* objects = [NSArray arrayWithObject:@"first object"]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat([section objectAtIndex:0], is(equalTo(@"first object"))); +} + +- (void)testInsertTheObjectAtAGivenIndex { + NSArray* objects = [NSArray arrayWithObject:@"first object"]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat([section objectAtIndex:0], is(equalTo(@"first object"))); + [section insertObject:@"inserted object" atIndex:0]; + assertThat([section objectAtIndex:0], is(equalTo(@"inserted object"))); +} + +- (void)testRemoveTheObjectAtAGivenIndex { + NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat([section objectAtIndex:0], is(equalTo(@"first object"))); + assertThat([section objectAtIndex:1], is(equalTo(@"second object"))); + [section removeObjectAtIndex:0]; + assertThat([section objectAtIndex:0], is(equalTo(@"second object"))); +} + +- (void)testReplaceTheObjectAtAGivenIndex { + NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat([section objectAtIndex:0], is(equalTo(@"first object"))); + assertThat([section objectAtIndex:1], is(equalTo(@"second object"))); + [section replaceObjectAtIndex:0 withObject:@"new first object"]; + assertThat([section objectAtIndex:0], is(equalTo(@"new first object"))); +} + +- (void)testMoveTheObjectAtAGivenIndex { + NSArray* objects = [NSArray arrayWithObjects:@"first object", @"second object", nil]; + RKTableViewCellMappings* mappings = [RKTableViewCellMappings new]; + RKTableSection* section = [RKTableSection sectionForObjects:objects withMappings:mappings]; + assertThat([section objectAtIndex:0], is(equalTo(@"first object"))); + assertThat([section objectAtIndex:1], is(equalTo(@"second object"))); + [section moveObjectAtIndex:1 toIndex:0]; + assertThat([section objectAtIndex:0], is(equalTo(@"second object"))); + assertThat([section objectAtIndex:1], is(equalTo(@"first object"))); +} + +@end diff --git a/Specs/Fixtures/Assets/blake.png b/Tests/Fixtures/Assets/blake.png similarity index 100% rename from Specs/Fixtures/Assets/blake.png rename to Tests/Fixtures/Assets/blake.png diff --git a/Tests/Fixtures/JSON/ArrayOfHumans.json b/Tests/Fixtures/JSON/ArrayOfHumans.json new file mode 100644 index 0000000000..cbc34c1c0f --- /dev/null +++ b/Tests/Fixtures/JSON/ArrayOfHumans.json @@ -0,0 +1,38 @@ +[ + { + "human": { + "id": 201, + "name": "Blake", + "human_id": 1, + "cats": [ + { + "cat": { + "id": 254, + "sku_id": 149 + } + }, + { + "order_component": { + "id": 255, + "sku_id": 145 + } + } + ] + } + }, + { + "human": { + "id": 202, + "name": "Sarah", + "human_id": 1, + "cats": [ + { + "order_component": { + "id": 256, + "sku_id": 144 + } + } + ] + } + } +] diff --git a/Specs/Fixtures/JSON/ArrayOfNestedDictionaries.json b/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json similarity index 99% rename from Specs/Fixtures/JSON/ArrayOfNestedDictionaries.json rename to Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json index 1d85aad6f8..ded2caf2fc 100644 --- a/Specs/Fixtures/JSON/ArrayOfNestedDictionaries.json +++ b/Tests/Fixtures/JSON/ArrayOfNestedDictionaries.json @@ -26,4 +26,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/Specs/Fixtures/JSON/ArrayOfResults.json b/Tests/Fixtures/JSON/ArrayOfResults.json similarity index 99% rename from Specs/Fixtures/JSON/ArrayOfResults.json rename to Tests/Fixtures/JSON/ArrayOfResults.json index b7f67129f7..2af487cbd1 100644 --- a/Specs/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/Specs/Fixtures/JSON/ComplexNestedUser.json b/Tests/Fixtures/JSON/ComplexNestedUser.json similarity index 100% rename from Specs/Fixtures/JSON/ComplexNestedUser.json rename to Tests/Fixtures/JSON/ComplexNestedUser.json diff --git a/Specs/Fixtures/JSON/ConnectingParents.json b/Tests/Fixtures/JSON/ConnectingParents.json similarity index 99% rename from Specs/Fixtures/JSON/ConnectingParents.json rename to Tests/Fixtures/JSON/ConnectingParents.json index 3945fe047a..4b78cfe2f4 100644 --- a/Specs/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 new file mode 100644 index 0000000000..df95b9e139 --- /dev/null +++ b/Tests/Fixtures/JSON/Dynamic/boy.json @@ -0,0 +1 @@ +{ "name": "Blake Watters", "type": "Boy", "numeric_type": 1 } diff --git a/Tests/Fixtures/JSON/Dynamic/child.json b/Tests/Fixtures/JSON/Dynamic/child.json new file mode 100644 index 0000000000..1a85ebccaf --- /dev/null +++ b/Tests/Fixtures/JSON/Dynamic/child.json @@ -0,0 +1 @@ +{ "name": "Gargamel", "type": "Child", "id": 2 } diff --git a/Specs/Fixtures/JSON/Dynamic/friends.json b/Tests/Fixtures/JSON/Dynamic/friends.json similarity index 99% rename from Specs/Fixtures/JSON/Dynamic/friends.json rename to Tests/Fixtures/JSON/Dynamic/friends.json index 83debc9861..0f1f64fab6 100644 --- a/Specs/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 new file mode 100644 index 0000000000..db10e7cf63 --- /dev/null +++ b/Tests/Fixtures/JSON/Dynamic/girl.json @@ -0,0 +1 @@ +{ "name": "Sarah", "type": "Girl", "numeric_type": 0 } diff --git a/Specs/Fixtures/JSON/Dynamic/mixed.json b/Tests/Fixtures/JSON/Dynamic/mixed.json similarity index 53% rename from Specs/Fixtures/JSON/Dynamic/mixed.json rename to Tests/Fixtures/JSON/Dynamic/mixed.json index 51dfb6f321..649e333a79 100644 --- a/Specs/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 new file mode 100644 index 0000000000..6c83335ce4 --- /dev/null +++ b/Tests/Fixtures/JSON/Dynamic/parent.json @@ -0,0 +1 @@ +{ "name": "Dan", "type": "Parent", "age": 33, "id": 1 } diff --git a/Specs/Fixtures/JSON/DynamicKeys.json b/Tests/Fixtures/JSON/DynamicKeys.json similarity index 99% rename from Specs/Fixtures/JSON/DynamicKeys.json rename to Tests/Fixtures/JSON/DynamicKeys.json index 6b944852a3..50c6df0b2b 100644 --- a/Specs/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/Specs/Fixtures/JSON/DynamicKeysWithNestedRelationship.json b/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json similarity index 96% rename from Specs/Fixtures/JSON/DynamicKeysWithNestedRelationship.json rename to Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json index 69da4ab020..e69f381df1 100644 --- a/Specs/Fixtures/JSON/DynamicKeysWithNestedRelationship.json +++ b/Tests/Fixtures/JSON/DynamicKeysWithNestedRelationship.json @@ -1,6 +1,6 @@ { "groups":[ - { + { "name":"restkit", "users": { "blake": { @@ -17,7 +17,7 @@ } } }, - { + { "name":"others", "users":{ "bjorn": { @@ -30,4 +30,4 @@ } } ] -} \ No newline at end of file +} diff --git a/Specs/Fixtures/JSON/DynamicKeysWithRelationship.json b/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json similarity index 99% rename from Specs/Fixtures/JSON/DynamicKeysWithRelationship.json rename to Tests/Fixtures/JSON/DynamicKeysWithRelationship.json index f5a8a9f1c4..7ae1fe6d5b 100644 --- a/Specs/Fixtures/JSON/DynamicKeysWithRelationship.json +++ b/Tests/Fixtures/JSON/DynamicKeysWithRelationship.json @@ -13,4 +13,4 @@ } } } -} \ No newline at end of file +} diff --git a/Specs/Fixtures/JSON/Foursquare.json b/Tests/Fixtures/JSON/Foursquare.json similarity index 100% rename from Specs/Fixtures/JSON/Foursquare.json rename to Tests/Fixtures/JSON/Foursquare.json diff --git a/Tests/Fixtures/JSON/NakedEvents.json b/Tests/Fixtures/JSON/NakedEvents.json new file mode 100644 index 0000000000..09d92f41c2 --- /dev/null +++ b/Tests/Fixtures/JSON/NakedEvents.json @@ -0,0 +1,14 @@ +[ + { + "type": "Meeting", + "event_id": "EV2321", + "location": "Building 3", + "summary": "Senior staff meeting" + }, + { + "type": "Party", + "event_id": "EV4432", + "location": "Your House", + "summary": "Hootnanny" + } +] diff --git a/Specs/Fixtures/JSON/RailsUser.json b/Tests/Fixtures/JSON/RailsUser.json similarity index 99% rename from Specs/Fixtures/JSON/RailsUser.json rename to Tests/Fixtures/JSON/RailsUser.json index 3a5c90ef10..45386105b1 100644 --- a/Specs/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/Specs/Fixtures/JSON/SameKeyDifferentTargetClasses.json b/Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json similarity index 100% rename from Specs/Fixtures/JSON/SameKeyDifferentTargetClasses.json rename to Tests/Fixtures/JSON/SameKeyDifferentTargetClasses.json 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 new file mode 100644 index 0000000000..50538a24cc --- /dev/null +++ b/Tests/Fixtures/JSON/error.json @@ -0,0 +1 @@ +{error: "this is an error"} diff --git a/Tests/Fixtures/JSON/errors.json b/Tests/Fixtures/JSON/errors.json new file mode 100644 index 0000000000..97107d2bfd --- /dev/null +++ b/Tests/Fixtures/JSON/errors.json @@ -0,0 +1 @@ +{ "errors" : ["error1", "error2"] } diff --git a/Specs/Fixtures/JSON/humans/1.json b/Tests/Fixtures/JSON/humans/1.json similarity index 54% rename from Specs/Fixtures/JSON/humans/1.json rename to Tests/Fixtures/JSON/humans/1.json index 62ba362129..96d2488cba 100644 --- a/Specs/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/Specs/Fixtures/JSON/humans/all.json b/Tests/Fixtures/JSON/humans/all.json similarity index 83% rename from Specs/Fixtures/JSON/humans/all.json rename to Tests/Fixtures/JSON/humans/all.json index ba80f2f8a8..d801180774 100644 --- a/Specs/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/Specs/Fixtures/JSON/humans/with_to_one_relationship.json b/Tests/Fixtures/JSON/humans/with_to_one_relationship.json similarity index 74% rename from Specs/Fixtures/JSON/humans/with_to_one_relationship.json rename to Tests/Fixtures/JSON/humans/with_to_one_relationship.json index de06d9467a..3c2b500fac 100644 --- a/Specs/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/Specs/Fixtures/JSON/nested_user.json b/Tests/Fixtures/JSON/nested_user.json similarity index 97% rename from Specs/Fixtures/JSON/nested_user.json rename to Tests/Fixtures/JSON/nested_user.json index fb5e20f739..882493ddc2 100644 --- a/Specs/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/Specs/Fixtures/JSON/user.json b/Tests/Fixtures/JSON/user.json similarity index 74% rename from Specs/Fixtures/JSON/user.json rename to Tests/Fixtures/JSON/user.json index a38861aaa1..b0ef7c16b0 100644 --- a/Specs/Fixtures/JSON/user.json +++ b/Tests/Fixtures/JSON/user.json @@ -2,28 +2,29 @@ "id": 31337, "name": "Blake Watters", "birthdate": "11/27/1982", + "favorite_date": "03/01/2012", "website": "http://restkit.org/", "is_developer": "true", "weight": "131.3", "lucky_number": "187", "interests": [ "Hacking", - "Running" + "Running" ], "address": { "id": 1234, "city": "Carrboro", "state": "North Carolina", - "country": "USA" + "country": "USA" }, "friends": [ { "id": 187, - "name": "Jeremy Ellison" + "name": "Jeremy Ellison" }, { "id": 7, - "name": "Rachit Shukla" + "name": "Rachit Shukla" } ] -} \ No newline at end of file +} diff --git a/Specs/Fixtures/JSON/users.json b/Tests/Fixtures/JSON/users.json similarity index 100% rename from Specs/Fixtures/JSON/users.json rename to Tests/Fixtures/JSON/users.json diff --git a/Tests/Fixtures/Uploads/.gitignore b/Tests/Fixtures/Uploads/.gitignore new file mode 100644 index 0000000000..72e8ffc0db --- /dev/null +++ b/Tests/Fixtures/Uploads/.gitignore @@ -0,0 +1 @@ +* diff --git a/Specs/Fixtures/XML/attributes_without_text_content.xml b/Tests/Fixtures/XML/attributes_without_text_content.xml similarity index 100% rename from Specs/Fixtures/XML/attributes_without_text_content.xml rename to Tests/Fixtures/XML/attributes_without_text_content.xml diff --git a/Tests/Fixtures/XML/channels.xml b/Tests/Fixtures/XML/channels.xml new file mode 100644 index 0000000000..6e637bd104 --- /dev/null +++ b/Tests/Fixtures/XML/channels.xml @@ -0,0 +1,11 @@ + + + + http://domain.com/Images/MySpecialTitle.png + http://domain.com/Images/65x35/2234.png + MySpecialTitle + it + + + + diff --git a/Specs/Fixtures/XML/container_attributes.xml b/Tests/Fixtures/XML/container_attributes.xml similarity index 100% rename from Specs/Fixtures/XML/container_attributes.xml rename to Tests/Fixtures/XML/container_attributes.xml diff --git a/Specs/Fixtures/XML/national_weather_service.xml b/Tests/Fixtures/XML/national_weather_service.xml similarity index 100% rename from Specs/Fixtures/XML/national_weather_service.xml rename to Tests/Fixtures/XML/national_weather_service.xml diff --git a/Specs/Fixtures/XML/orders.xml b/Tests/Fixtures/XML/orders.xml similarity index 100% rename from Specs/Fixtures/XML/orders.xml rename to Tests/Fixtures/XML/orders.xml diff --git a/Specs/Fixtures/XML/tab_data.xml b/Tests/Fixtures/XML/tab_data.xml similarity index 100% rename from Specs/Fixtures/XML/tab_data.xml rename to Tests/Fixtures/XML/tab_data.xml diff --git a/Specs/Fixtures/XML/zend.xml b/Tests/Fixtures/XML/zend.xml similarity index 100% rename from Specs/Fixtures/XML/zend.xml rename to Tests/Fixtures/XML/zend.xml diff --git a/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m new file mode 100644 index 0000000000..4d8080de4a --- /dev/null +++ b/Tests/Logic/CoreData/NSEntityDescription+RKAdditionsTest.m @@ -0,0 +1,140 @@ +// +// NSEntityDescription+RKAdditionsTest.m +// RestKit +// +// Created by Blake Watters on 3/22/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" + +@interface NSEntityDescription_RKAdditionsTest : RKTestCase + +@end + +@implementation NSEntityDescription_RKAdditionsTest + +- (void)testRetrievalOfPrimaryKeyFromXcdatamodel +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"railsID"))); +} + +- (void)testRetrievalOfUnconfiguredPrimaryKeyAttributeReturnsNil +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(entity.primaryKeyAttribute, is(nilValue())); +} + +- (void)testSettingPrimaryKeyAttributeNameProgramatically +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKHouse" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + entity.primaryKeyAttributeName = @"houseID"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"houseID"))); +} + +- (void)testSettingExistingPrimaryKeyAttributeNameProgramatically +{ + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + 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 new file mode 100644 index 0000000000..7c5c1a4c5b --- /dev/null +++ b/Tests/Logic/CoreData/NSManagedObject+ActiveRecordTest.m @@ -0,0 +1,65 @@ +// +// NSManagedObject+ActiveRecordTest.m +// RestKit +// +// Created by Blake Watters on 3/22/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKHuman.h" + +@interface NSManagedObject_ActiveRecordTest : SenTestCase + +@end + +@implementation NSManagedObject_ActiveRecordTest + +- (void)testFindByPrimaryKey +{ + 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:[NSNumber numberWithInt:12345] inContext:store.primaryManagedObjectContext]; + assertThat(foundHuman, is(equalTo(human))); +} + +- (void)testFindByPrimaryKeyInContext +{ + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSManagedObjectContext *context = [[RKTestFactory managedObjectStore] newManagedObjectContext]; + NSEntityDescription *entity = [RKHuman entityDescription]; + entity.primaryKeyAttributeName = @"railsID"; + + RKHuman *human = [RKHuman createInContext:context]; + human.railsID = [NSNumber numberWithInt:12345]; + [context save:nil]; + + RKHuman *foundHuman = [RKHuman findByPrimaryKey:[NSNumber numberWithInt:12345] inContext:store.primaryManagedObjectContext]; + assertThat(foundHuman, is(nilValue())); + + foundHuman = [RKHuman findByPrimaryKey:[NSNumber numberWithInt:12345] inContext:context]; + 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 new file mode 100644 index 0000000000..c5270d7dce --- /dev/null +++ b/Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m @@ -0,0 +1,60 @@ +// +// RKFetchRequestMappingTest.m +// RestKit +// +// Created by Blake Watters on 3/20/12. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKCat.h" +#import "RKEvent.h" + +@interface RKFetchRequestMappingCacheTest : RKTestCase + +@end + +@implementation RKFetchRequestMappingCacheTest + +- (void)testFetchRequestMappingCacheReturnsObjectsWithNumericPrimaryKey +{ + // RKCat entity. Integer prinmary key. + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKFetchRequestManagedObjectCache *cache = [RKFetchRequestManagedObjectCache new]; + 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 + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:[NSNumber numberWithInt:123456] + inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(cachedObject, is(equalTo(reginald))); +} + +- (void)testFetchRequestMappingCacheReturnsObjectsWithStringPrimaryKey +{ + // 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" + inManagedObjectContext:objectStore.primaryManagedObjectContext]; + assertThat(cachedObject, is(equalTo(birthday))); +} + +@end diff --git a/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m b/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m new file mode 100644 index 0000000000..efd461f15f --- /dev/null +++ b/Tests/Logic/CoreData/RKManagedObjectLoaderTest.m @@ -0,0 +1,238 @@ +// +// RKManagedObjectLoaderTest.m +// RestKit +// +// Created by Blake Watters on 4/28/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKManagedObjectLoader.h" +#import "RKManagedObjectMapping.h" +#import "RKHuman.h" +#import "RKCat.h" +#import "NSManagedObject+ActiveRecord.h" +#import "RKObjectMappingProvider+CoreData.h" + +@interface RKManagedObjectLoaderTest : RKTestCase { + +} + +@end + +@implementation RKManagedObjectLoaderTest + +- (void)testShouldDeleteObjectFromLocalStoreOnDELETE { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + [store save:nil]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = store; + RKHuman* human = [RKHuman object]; + human.name = @"Blake Watters"; + human.railsID = [NSNumber numberWithInt:1]; + [objectManager.objectStore save:nil]; + + assertThat(objectManager.objectStore.primaryManagedObjectContext, is(equalTo(store.primaryManagedObjectContext))); + + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKURL *URL = [objectManager.baseURL URLByAppendingResourcePath:@"/humans/1"]; + RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithURL:URL mappingProvider:objectManager.mappingProvider objectStore:store]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodDELETE; + objectLoader.objectMapping = mapping; + objectLoader.targetObject = human; + [objectLoader send]; + [responseLoader waitForResponse]; + assertThatBool([human isDeleted], equalToBool(YES)); +} + +- (void)testShouldLoadAnObjectWithAToOneRelationship { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = store; + + RKObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + [humanMapping mapAttributes:@"name", nil]; + RKObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:store]; + [catMapping mapAttributes:@"name", nil]; + [humanMapping mapKeyPath:@"favorite_cat" toRelationship:@"favoriteCat" withMapping:catMapping]; + [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKURL *URL = [objectManager.baseURL URLByAppendingResourcePath:@"/JSON/humans/with_to_one_relationship.json"]; + RKManagedObjectLoader* objectLoader = [RKManagedObjectLoader loaderWithURL:URL mappingProvider:objectManager.mappingProvider objectStore:store]; + objectLoader.delegate = responseLoader; + [objectLoader send]; + [responseLoader waitForResponse]; + RKHuman* human = [responseLoader.objects lastObject]; + assertThat(human, isNot(nilValue())); + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (void)testShouldDeleteObjectsMissingFromPayloadReturnedByObjectCache { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman" + inManagedObjectStore:store]; + [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; + [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))); + RKHuman* blake = [RKHuman createEntity]; + blake.railsID = [NSNumber numberWithInt:123]; + RKHuman* other = [RKHuman createEntity]; + other.railsID = [NSNumber numberWithInt:456]; + RKHuman* deleteMe = [RKHuman createEntity]; + deleteMe.railsID = [NSNumber numberWithInt:9999]; + [store save:nil]; + assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(3))); + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.mappingProvider setObjectMapping:humanMapping + forResourcePathPattern:@"/JSON/humans/all.json" + withFetchRequestBlock:^ (NSString *resourcePath) { + return [RKHuman fetchRequest]; + }]; + objectManager.objectStore = store; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + responseLoader.timeout = 25; + RKURL *URL = [objectManager.baseURL URLByAppendingResourcePath:@"/JSON/humans/all.json"]; + RKManagedObjectLoader *objectLoader = [RKManagedObjectLoader loaderWithURL:URL mappingProvider:objectManager.mappingProvider objectStore:store]; + objectLoader.delegate = responseLoader; + [objectLoader send]; + [responseLoader waitForResponse]; + + assertThatUnsignedInteger([RKHuman count:nil], is(equalToInt(2))); + assertThatBool([blake isDeleted], is(equalToBool(NO))); + assertThatBool([other isDeleted], is(equalToBool(NO))); + assertThatBool([deleteMe isDeleted], is(equalToBool(YES))); +} + +- (void)testShouldNotAssertDuringObjectMappingOnSynchronousRequest { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = store; + + RKObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + RKManagedObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/humans/1"]; + objectLoader.objectMapping = mapping; + RKResponse *response = [objectLoader sendSynchronously]; + + NSArray* humans = [RKHuman findAll]; + assertThatUnsignedInteger([humans count], is(equalToInt(1))); + assertThatInteger(response.statusCode, is(equalToInt(200))); +} + +- (void)testShouldSkipObjectMappingOnRequestCacheHitWhenObjectCachePresent { + [RKTestFactory clearCacheDirectory]; + + RKObjectManager *objectManager = [RKTestFactory objectManager]; + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + objectManager.objectStore = objectStore; + RKManagedObjectMapping *humanMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKHuman" inManagedObjectStore:objectStore]; + [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; + [humanMapping mapAttributes:@"name", nil]; + humanMapping.primaryKeyAttribute = @"railsID"; + humanMapping.rootKeyPath = @"human"; + + [RKHuman truncateAll]; + assertThatInteger([RKHuman count:nil], is(equalToInteger(0))); + RKHuman *blake = [RKHuman createEntity]; + blake.railsID = [NSNumber numberWithInt:123]; + RKHuman *other = [RKHuman createEntity]; + other.railsID = [NSNumber numberWithInt:456]; + [objectStore save:nil]; + assertThatInteger([RKHuman count:nil], is(equalToInteger(2))); + + [objectManager.mappingProvider setMapping:humanMapping forKeyPath:@"human"]; + [objectManager.mappingProvider setObjectMapping:humanMapping forResourcePathPattern:@"/coredata/etag" withFetchRequestBlock:^NSFetchRequest *(NSString *resourcePath) { + return [RKHuman fetchRequest]; + }]; + + { + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + RKManagedObjectLoader *objectLoader = [objectManager loaderWithResourcePath:@"/coredata/etag"]; + objectLoader.delegate = responseLoader; + id mockLoader = [OCMockObject partialMockForObject:objectLoader]; + [[[mockLoader expect] andForwardToRealObject] performMapping:[OCMArg setTo:OCMOCK_ANY]]; + + [mockLoader send]; + [responseLoader waitForResponse]; + + STAssertNoThrow([mockLoader verify], nil); + assertThatInteger([RKHuman count:nil], is(equalToInteger(2))); + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThatBool([responseLoader.response wasLoadedFromCache], is(equalToBool(NO))); + assertThatInteger([responseLoader.objects count], is(equalToInteger(2))); + } + { + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + RKManagedObjectLoader *objectLoader = [objectManager loaderWithResourcePath:@"/coredata/etag"]; + objectLoader.delegate = responseLoader; + id mockLoader = [OCMockObject partialMockForObject:objectLoader]; + [[mockLoader reject] performMapping:[OCMArg setTo:OCMOCK_ANY]]; + + [mockLoader send]; + [responseLoader waitForResponse]; + + STAssertNoThrow([mockLoader verify], nil); + assertThatInteger([RKHuman count:nil], is(equalToInteger(2))); + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThatBool([responseLoader.response wasLoadedFromCache], is(equalToBool(YES))); + assertThatInteger([responseLoader.objects count], is(equalToInteger(2))); + } +} + +- (void)testTheOnDidFailBlockIsInvokedOnFailure { + RKObjectManager *objectManager = [RKTestFactory objectManager]; + RKManagedObjectLoader *loader = [objectManager loaderWithResourcePath:@"/fail"]; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + __block BOOL invoked = NO; + loader.onDidFailWithError = ^ (NSError *error) { + invoked = YES; + }; + loader.delegate = responseLoader; + [loader sendAsynchronously]; + [responseLoader waitForResponse]; + 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 new file mode 100644 index 0000000000..7eaded59f0 --- /dev/null +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m @@ -0,0 +1,493 @@ +// +// RKManagedObjectMappingOperationTest.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKManagedObjectMapping.h" +#import "RKManagedObjectMappingOperation.h" +#import "RKCat.h" +#import "RKHuman.h" +#import "RKChild.h" +#import "RKParent.h" +#import "RKBenchmark.h" + +@interface RKManagedObjectMappingOperationTest : RKTestCase { + +} + +@end + +@implementation RKManagedObjectMappingOperationTest + +- (void)testShouldOverloadInitializationOfRKObjectMappingOperationToReturnInstancesOfRKManagedObjectMappingOperationWhenAppropriate { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* managedMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + NSDictionary* sourceObject = [NSDictionary dictionary]; + RKHuman* human = [RKHuman createEntity]; + RKObjectMappingOperation* operation = [RKObjectMappingOperation mappingOperationFromObject:sourceObject toObject:human withMapping:managedMapping]; + assertThat(operation, is(instanceOf([RKManagedObjectMappingOperation class]))); +} + +- (void)testShouldOverloadInitializationOfRKObjectMappingOperationButReturnUnmanagedMappingOperationWhenAppropriate { + RKObjectMapping* vanillaMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + NSDictionary* sourceObject = [NSDictionary dictionary]; + NSMutableDictionary* destinationObject = [NSMutableDictionary dictionary]; + RKObjectMappingOperation* operation = [RKObjectMappingOperation mappingOperationFromObject:sourceObject toObject:destinationObject withMapping:vanillaMapping]; + assertThat(operation, is(instanceOf([RKObjectMappingOperation class]))); +} + +- (void)testShouldConnectRelationshipsByPrimaryKey { + 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]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (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]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (void)testShouldConnectRelationshipsByPrimaryKeyWithDifferentSourceAndDestinationKeyPaths { + 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", @"catIDs", nil]; + [humanMapping mapRelationship:@"cats" withMapping:catMapping]; + [humanMapping connectRelationship:@"cats" withObjectForPrimaryKeyAttribute:@"catIDs"]; + + // Create a couple of cats to connect + 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]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.cats, isNot(nilValue())); + assertThat([human.cats valueForKeyPath:@"name"], containsInAnyOrder(@"Asia", @"Reginald Royford Williams III", nil)); +} + +- (void)testShouldLoadNestedHasManyRelationship { + 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 hasMany:@"cats" 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]; + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); +} + +- (void)testShouldLoadOrderedHasManyRelationship { + 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 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]; + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat([human catsInOrderByAge], isNot(empty())); +} + +- (void)testShouldMapNullToAHasManyRelationship { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; + [catMapping mapAttributes:@"name", nil]; + + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; + [humanMapping mapAttributes:@"name", @"favoriteCatID", nil]; + [humanMapping hasMany:@"cats" withMapping:catMapping]; + + NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Blake", @"cats", [NSNull null], nil]; + RKHuman* human = [RKHuman object]; + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:human mapping:humanMapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.cats, is(empty())); +} + +- (void)testShouldLoadNestedHasManyRelationshipWithoutABackingClass { + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* cloudMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKCloud" inManagedObjectStore:objectStore]; + [cloudMapping mapAttributes:@"name", nil]; + + RKManagedObjectMapping* stormMapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKStorm" inManagedObjectStore:objectStore]; + [stormMapping mapAttributes:@"name", @"favoriteCatID", nil]; + [stormMapping hasMany:@"clouds" withMapping:cloudMapping]; + + NSArray* cloudsData = [NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"Nimbus" forKey:@"name"]]; + NSDictionary* mappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Hurricane", @"clouds", cloudsData, nil]; + NSEntityDescription* entity = [NSEntityDescription entityForName:@"RKStorm" inManagedObjectContext:objectStore.primaryManagedObjectContext]; + NSManagedObject* storm = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:objectStore.primaryManagedObjectContext]; + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:mappableData destinationObject:storm mapping:stormMapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); +} + +- (void)testShouldDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingSucceeds { + 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" whenValueOfKeyPath:@"name" isEqualTo:@"Blake"]; + + // 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]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.favoriteCat, isNot(nilValue())); + assertThat(human.favoriteCat.name, is(equalTo(@"Asia"))); +} + +- (void)testShouldNotDynamicallyConnectRelationshipsByPrimaryKeyWhenMatchingFails { + 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" whenValueOfKeyPath:@"name" isEqualTo:@"Jeff"]; + + // 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]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(human.favoriteCat, is(nilValue())); +} + +- (void)testShouldConnectManyToManyRelationships { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"railsID"; + [childMapping mapAttributes:@"name", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + parentMapping.primaryKeyAttribute = @"railsID"; + [parentMapping mapAttributes:@"name", @"age", nil]; + [parentMapping hasMany:@"children" withMapping:childMapping]; + + NSArray* childMappableData = [NSArray arrayWithObjects:[NSDictionary dictionaryWithKeysAndObjects:@"name", @"Maya", nil], + [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Brady", nil], nil]; + NSDictionary* parentMappableData = [NSDictionary dictionaryWithKeysAndObjects:@"name", @"Win", + @"age", [NSNumber numberWithInt:34], + @"children", childMappableData, nil]; + RKParent* parent = [RKParent object]; + RKManagedObjectMappingOperation* operation = [[RKManagedObjectMappingOperation alloc] initWithSourceObject:parentMappableData destinationObject:parent mapping:parentMapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(parent.children, isNot(nilValue())); + assertThatUnsignedInteger([parent.children count], is(equalToInt(2))); + assertThat([[parent.children anyObject] parents], isNot(nilValue())); + assertThatBool([[[parent.children anyObject] parents] containsObject:parent], is(equalToBool(YES))); + assertThatUnsignedInteger([[[parent.children anyObject] parents] count], is(equalToInt(1))); +} + +- (void)testShouldConnectRelationshipsByPrimaryKeyRegardlessOfOrder { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + [childMapping mapAttributes:@"fatherID", nil]; + [childMapping mapRelationship:@"father" withMapping:parentMapping]; + [childMapping connectRelationship:@"father" withObjectForPrimaryKeyAttribute:@"fatherID"]; + + 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 setMapping:parentMapping forKeyPath:@"parents"]; + [mappingProvider setMapping:childMapping forKeyPath:@"children"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + RKObjectMappingResult *result = [mapper performMapping]; + NSArray *children = [[result asDictionary] valueForKey:@"children"]; + assertThat(children, hasCountOf(1)); + RKChild *child = [children lastObject]; + 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 new file mode 100644 index 0000000000..4e05176ba9 --- /dev/null +++ b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m @@ -0,0 +1,346 @@ +// +// RKManagedObjectMappingTest.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKManagedObjectMapping.h" +#import "RKHuman.h" +#import "RKMappableObject.h" +#import "RKChild.h" +#import "RKParent.h" +#import "NSEntityDescription+RKAdditions.h" + +@interface RKManagedObjectMappingTest : RKTestCase + +@end + +@implementation RKManagedObjectMappingTest + +- (void)setUp +{ + [RKTestFactory setUp]; +} + +- (void)tearDown +{ + [RKTestFactory tearDown]; +} + +- (void)testShouldReturnTheDefaultValueForACoreDataAttribute { + // Load Core Data + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForEntityWithName:@"RKCat" inManagedObjectStore:store]; + id value = [mapping defaultValueForMissingAttribute:@"name"]; + assertThat(value, is(equalTo(@"Kitty Cat!"))); +} + +- (void)testShouldCreateNewInstancesOfUnmanagedObjects { + [RKTestFactory managedObjectStore]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKMappableObject class]]; + id object = [mapping mappableObjectForData:[NSDictionary dictionary]]; + assertThat(object, isNot(nilValue())); + assertThat([object class], is(equalTo([RKMappableObject class]))); +} + +- (void)testShouldCreateNewInstancesOfManagedObjectsWhenTheMappingIsAnRKObjectMapping { + [RKTestFactory managedObjectStore]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKMappableObject class]]; + id object = [mapping mappableObjectForData:[NSDictionary dictionary]]; + assertThat(object, isNot(nilValue())); + assertThat([object class], is(equalTo([RKMappableObject class]))); +} + +- (void)testShouldCreateNewManagedObjectInstancesWhenThereIsNoPrimaryKeyInTheData { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"railsID"; + + NSDictionary* data = [NSDictionary dictionary]; + id object = [mapping mappableObjectForData:data]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(instanceOf([RKHuman class]))); +} + +- (void)testShouldCreateNewManagedObjectInstancesWhenThereIsNoPrimaryKeyAttribute { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + + NSDictionary* data = [NSDictionary dictionary]; + id object = [mapping mappableObjectForData:data]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(instanceOf([RKHuman class]))); +} + +- (void)testShouldCreateANewManagedObjectWhenThePrimaryKeyValueIsNSNull { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; + + NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNull null] forKey:@"id"]; + id object = [mapping mappableObjectForData:data]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(instanceOf([RKHuman class]))); +} + +- (void)testShouldMapACollectionOfObjectsWithDynamicKeys { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; + mapping.forceCollectionMapping = YES; + mapping.primaryKeyAttribute = @"name"; + [mapping mapKeyOfNestedDictionaryToAttribute:@"name"]; + RKObjectAttributeMapping *idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"(name).id" toKeyPath:@"railsID"]; + [mapping addAttributeMapping:idMapping]; + RKObjectMappingProvider *provider = [[RKObjectMappingProvider new] autorelease]; + [provider setMapping:mapping forKeyPath:@"users"]; + + id mockCacheStrategy = [OCMockObject partialMockForObject:objectStore.cacheStrategy]; + [[[mockCacheStrategy expect] andForwardToRealObject] findInstanceOfEntity:OCMOCK_ANY + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:@"blake" + inManagedObjectContext:objectStore.primaryManagedObjectContext]; + [[[mockCacheStrategy expect] andForwardToRealObject] findInstanceOfEntity:mapping.entity + withPrimaryKeyAttribute:mapping.primaryKeyAttribute + value:@"rachit" + inManagedObjectContext:objectStore.primaryManagedObjectContext]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeys.json"]; + RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; + [mapper performMapping]; + [mockCacheStrategy verify]; +} + +- (void)testShouldPickTheAppropriateMappingBasedOnAnAttributeValue { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:objectStore]; + childMapping.primaryKeyAttribute = @"railsID"; + [childMapping mapAttributes:@"name", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:objectStore]; + parentMapping.primaryKeyAttribute = @"railsID"; + [parentMapping mapAttributes:@"name", @"age", nil]; + + [dynamicMapping setObjectMapping:parentMapping whenValueOfKeyPath:@"type" isEqualTo:@"Parent"]; + [dynamicMapping setObjectMapping:childMapping whenValueOfKeyPath:@"type" isEqualTo:@"Child"]; + + RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"parent.json"]]; + assertThat(mapping, is(notNilValue())); + assertThatBool([mapping isKindOfClass:[RKManagedObjectMapping class]], is(equalToBool(YES))); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"RKParent"))); + mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"child.json"]]; + assertThat(mapping, is(notNilValue())); + assertThatBool([mapping isKindOfClass:[RKManagedObjectMapping class]], is(equalToBool(YES))); + assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"RKChild"))); +} + +- (void)testShouldIncludeTransformableAttributesInPropertyNamesAndTypes { + [RKTestFactory managedObjectStore]; + NSDictionary *attributesByName = [[RKHuman entity] attributesByName]; + NSDictionary *propertiesByName = [[RKHuman entity] propertiesByName]; + NSDictionary *relationshipsByName = [[RKHuman entity] relationshipsByName]; + assertThat([attributesByName objectForKey:@"favoriteColors"], is(notNilValue())); + assertThat([propertiesByName objectForKey:@"favoriteColors"], is(notNilValue())); + assertThat([relationshipsByName objectForKey:@"favoriteColors"], is(nilValue())); + + NSDictionary *propertyNamesAndTypes = [[RKObjectPropertyInspector sharedInspector] propertyNamesAndTypesForEntity:[RKHuman entity]]; + assertThat([propertyNamesAndTypes objectForKey:@"favoriteColors"], is(notNilValue())); +} + +- (void)testThatAssigningAnEntityWithANonNilPrimaryKeyAttributeSetsTheDefaultValueForTheMapping { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCat" inManagedObjectContext:store.primaryManagedObjectContext]; + RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForEntity:entity inManagedObjectStore:store]; + assertThat(mapping.primaryKeyAttribute, is(equalTo(@"railsID"))); +} + +- (void)testThatAssigningAPrimaryKeyAttributeToAMappingWhoseEntityHasANilPrimaryKeyAttributeAssignsItToTheEntity { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:store.primaryManagedObjectContext]; + RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForEntity:entity inManagedObjectStore:store]; + assertThat(mapping.primaryKeyAttribute, is(nilValue())); + mapping.primaryKeyAttribute = @"name"; + assertThat(entity.primaryKeyAttributeName, is(equalTo(@"name"))); + assertThat(entity.primaryKeyAttribute, is(notNilValue())); +} + +#pragma mark - Fetched Results Cache + +- (void)testShouldFindExistingManagedObjectsByPrimaryKeyWithFetchedResultsCache { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + 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())); + assertThat(object, is(equalTo(human))); +} + +- (void)testShouldFindExistingManagedObjectsByPrimaryKeyPathWithFetchedResultsCache { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + [RKHuman truncateAll]; + 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]; + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); +} + +#pragma mark - In Memory Cache + +- (void)testShouldFindExistingManagedObjectsByPrimaryKeyWithInMemoryCache { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + [RKHuman truncateAllInContext:store.primaryManagedObjectContext]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + RKManagedObjectMapping* mapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:store]; + mapping.primaryKeyAttribute = @"railsID"; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; + + 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"]; + NSManagedObject *object = [mapping mappableObjectForData:data]; + assertThat([object managedObjectContext], is(equalTo(store.primaryManagedObjectContext))); + assertThat(object, isNot(nilValue())); + assertThat(object, is(equalTo(human))); +} + +- (void)testShouldFindExistingManagedObjectsByPrimaryKeyPathWithInMemoryCache { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + [RKHuman truncateAllInContext:store.primaryManagedObjectContext]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + [RKHuman truncateAll]; + 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]; + assertThat(object, isNot(nilValue())); + 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 new file mode 100644 index 0000000000..6424ab5fe7 --- /dev/null +++ b/Tests/Logic/CoreData/RKManagedObjectStoreTest.m @@ -0,0 +1,58 @@ +// +// RKManagedObjectStoreTest.m +// RestKit +// +// Created by Blake Watters on 7/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKHuman.h" +#import "RKDirectory.h" + +@interface RKManagedObjectStoreTest : RKTestCase + +@end + +@implementation RKManagedObjectStoreTest + +- (void)testInstantiationOfNewManagedObjectContextAssociatesWithObjectStore +{ + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + NSManagedObjectContext *context = [store newManagedObjectContext]; + 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/Specs/CoreData/RKManagedObjectThreadSafeInvocationSpec.m b/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m similarity index 70% rename from Specs/CoreData/RKManagedObjectThreadSafeInvocationSpec.m rename to Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m index 8e02ee4dbc..1c9fc704f6 100644 --- a/Specs/CoreData/RKManagedObjectThreadSafeInvocationSpec.m +++ b/Tests/Logic/CoreData/RKManagedObjectThreadSafeInvocationTest.m @@ -1,16 +1,16 @@ // -// RKObjectDelegateNotifierSpec.h +// RKManagedObjectThreadSafeInvocationTest.h // RestKit // // Created by Blake Watters on 5/12/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,11 +18,11 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKHuman.h" #import "RKManagedObjectThreadSafeInvocation.h" -@interface RKManagedObjectThreadSafeInvocationSpec : RKSpec { +@interface RKManagedObjectThreadSafeInvocationTest : RKTestCase { NSMutableDictionary* _dictionary; RKManagedObjectStore* _objectStore; id _results; @@ -31,10 +31,12 @@ @interface RKManagedObjectThreadSafeInvocationSpec : RKSpec { @end -@implementation RKManagedObjectThreadSafeInvocationSpec +@implementation RKManagedObjectThreadSafeInvocationTest - (void)testShouldSerializeOneManagedObjectToManagedObjectID { - RKSpecNewManagedObjectStore(); + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = objectStore; RKHuman* human = [RKHuman object]; NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithObject:human forKey:@"human"]; NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateWithDictionary:)]; @@ -43,8 +45,24 @@ - (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 { - RKSpecNewManagedObjectStore(); + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = objectStore; RKHuman* human1 = [RKHuman object]; RKHuman* human2 = [RKHuman object]; NSArray* humans = [NSArray arrayWithObjects:human1, human2, nil]; @@ -57,26 +75,30 @@ - (void)testShouldSerializeCollectionOfManagedObjectsToManagedObjectIDs { } - (void)testShouldDeserializeOneManagedObjectIDToManagedObject { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = objectStore; RKHuman* human = [RKHuman object]; NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithObject:[human objectID] forKey:@"human"]; NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateWithDictionary:)]; RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; - invocation.objectStore = store; + invocation.objectStore = objectStore; [invocation deserializeManagedObjectIDsForArgument:dictionary withKeyPaths:[NSSet setWithObject:@"human"]]; assertThat([dictionary valueForKeyPath:@"human"], is(instanceOf([NSManagedObject class]))); assertThat([dictionary valueForKeyPath:@"human"], is(equalTo(human))); } - (void)testShouldDeserializeCollectionOfManagedObjectIDToManagedObjects { - RKManagedObjectStore* store = RKSpecNewManagedObjectStore(); + RKManagedObjectStore* objectStore = [RKTestFactory managedObjectStore]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = objectStore; RKHuman* human1 = [RKHuman object]; RKHuman* human2 = [RKHuman object]; NSArray* humanIDs = [NSArray arrayWithObjects:[human1 objectID], [human2 objectID], nil]; NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithObject:humanIDs forKey:@"humans"]; NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateWithDictionary:)]; RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; - invocation.objectStore = store; + invocation.objectStore = objectStore; [invocation deserializeManagedObjectIDsForArgument:dictionary withKeyPaths:[NSSet setWithObject:@"humans"]]; assertThat([dictionary valueForKeyPath:@"humans"], is(instanceOf([NSArray class]))); NSArray* humans = [NSArray arrayWithObjects:human1, human2, nil]; @@ -94,13 +116,15 @@ - (void)informDelegateWithDictionary:(NSDictionary*)results { - (void)createBackgroundObjects { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; assertThatBool([NSThread isMainThread], equalToBool(NO)); - + // Assert this is not the main thread // Create a new array of objects in the background + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.objectStore = [RKTestFactory managedObjectStore]; NSArray* humans = [NSArray arrayWithObject:[RKHuman object]]; _dictionary = [[NSMutableDictionary dictionaryWithObject:humans forKey:@"humans"] retain]; NSMethodSignature* signature = [self methodSignatureForSelector:@selector(informDelegateWithDictionary:)]; - RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; + RKManagedObjectThreadSafeInvocation* invocation = [RKManagedObjectThreadSafeInvocation invocationWithMethodSignature:signature]; invocation.objectStore = _objectStore; [invocation retain]; [invocation setTarget:self]; @@ -108,18 +132,18 @@ - (void)createBackgroundObjects { [invocation setArgument:&_dictionary atIndex:2]; // NOTE: _cmd and self are 0 and 1 [invocation setManagedObjectKeyPaths:[NSSet setWithObject:@"humans"] forArgument:2]; [invocation invokeOnMainThread]; - + [pool drain]; } - (void)testShouldSerializeAndDeserializeManagedObjectsAcrossAThreadInvocation { - _objectStore = [RKSpecNewManagedObjectStore() retain]; + _objectStore = [[RKTestFactory managedObjectStore] retain]; _waiting = YES; [self performSelectorInBackground:@selector(createBackgroundObjects) withObject:nil]; - - while (_waiting) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - } + + while (_waiting) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } } @end diff --git a/Tests/Logic/CoreData/RKSearchWordObserverTest.m b/Tests/Logic/CoreData/RKSearchWordObserverTest.m new file mode 100644 index 0000000000..1c66295616 --- /dev/null +++ b/Tests/Logic/CoreData/RKSearchWordObserverTest.m @@ -0,0 +1,34 @@ +// +// RKSearchWordObserverTest.m +// RestKit +// +// Created by Blake Watters on 7/26/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKSearchWordObserver.h" +#import "RKSearchable.h" + +@interface RKSearchWordObserverTest : RKTestCase + +@end + +@implementation RKSearchWordObserverTest + +- (void)testInstantiateASearchWordObserverOnObjectStoreInit { + [RKTestFactory managedObjectStore]; + assertThat([RKSearchWordObserver sharedObserver], isNot(nil)); +} + +- (void)testTriggerSearchWordRegenerationForChagedSearchableValuesAtObjectContextSaveTime { + RKManagedObjectStore* store = [RKTestFactory managedObjectStore]; + RKSearchable* searchable = [RKSearchable createEntity]; + searchable.title = @"This is the title of my new object"; + assertThat(searchable.searchWords, is(empty())); + [store save:nil]; + assertThat(searchable.searchWords, isNot(empty())); + assertThat(searchable.searchWords, hasCountOf(8)); +} + +@end diff --git a/Tests/Logic/CoreData/RKSearchableManagedObjectTest.m b/Tests/Logic/CoreData/RKSearchableManagedObjectTest.m new file mode 100644 index 0000000000..3a75dd7b6e --- /dev/null +++ b/Tests/Logic/CoreData/RKSearchableManagedObjectTest.m @@ -0,0 +1,31 @@ +// +// RKSearchableManagedObjectTest.m +// RestKit +// +// Created by Blake Watters on 7/26/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSManagedObject+ActiveRecord.h" +#import "RKSearchable.h" + +@interface RKSearchableManagedObjectTest : RKTestCase + +@end + +@implementation RKSearchableManagedObjectTest + +- (void)testGenerateSearchWordsForSearchableObjects { + [RKTestFactory managedObjectStore]; + RKSearchable* searchable = [RKSearchable createEntity]; + searchable.title = @"This is the title of my new object"; + searchable.body = @"This is the point at which I begin pontificating at length about various and sundry things for no real reason at all. Furthermore, ..."; + assertThat(searchable.searchWords, is(empty())); + [searchable refreshSearchWords]; + assertThat(searchable.searchWords, isNot(empty())); + NSArray* words = [[[searchable.searchWords valueForKey:@"word"] allObjects] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + assertThat([words componentsJoinedByString:@", "], is(equalTo(@"about, all, and, at, begin, for, furthermore, i, is, length, my, new, no, object, of, point, pontificating, real, reason, sundry, the, things, this, title, various, which"))); +} + +@end diff --git a/Specs/Network/RKAuthenticationSpec.m b/Tests/Logic/Network/RKAuthenticationTest.m similarity index 58% rename from Specs/Network/RKAuthenticationSpec.m rename to Tests/Logic/Network/RKAuthenticationTest.m index e4585add1a..c55d9a2e3d 100644 --- a/Specs/Network/RKAuthenticationSpec.m +++ b/Tests/Logic/Network/RKAuthenticationTest.m @@ -1,16 +1,16 @@ // -// RKAuthenticationSpec.m +// RKAuthenticationTest.m // RestKit // // Created by Blake Watters on 3/14/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,55 +18,57 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKClient.h" -static NSString* const RKAuthenticationSpecUsername = @"restkit"; -static NSString* const RKAuthenticationSpecPassword = @"authentication"; +static NSString* const RKAuthenticationTestUsername = @"restkit"; +static NSString* const RKAuthenticationTestPassword = @"authentication"; + +@interface RKAuthenticationTest : RKTestCase { -@interface RKAuthenticationSpec : RKSpec { - } @end -@implementation RKAuthenticationSpec +@implementation RKAuthenticationTest - (void)testShouldAccessUnprotectedResourcePaths { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKClient* client = RKSpecNewClient(); + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKClient* client = [RKTestFactory client]; [client get:@"/authentication/none" delegate:loader]; [loader waitForResponse]; assertThatBool([loader.response isOK], is(equalToBool(YES))); } - (void)testShouldAuthenticateViaHTTPAuthBasic { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKClient* client = RKSpecNewClient(); - client.username = RKAuthenticationSpecUsername; - client.password = RKAuthenticationSpecPassword; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKClient* client = [RKTestFactory client]; + client.username = RKAuthenticationTestUsername; + client.password = RKAuthenticationTestPassword; [client get:@"/authentication/basic" delegate:loader]; [loader waitForResponse]; assertThatBool([loader.response isOK], is(equalToBool(YES))); } - (void)testShouldFailAuthenticationWithInvalidCredentialsForHTTPAuthBasic { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKClient* client = RKSpecNewClient(); - client.username = RKAuthenticationSpecUsername; + RKTestResponseLoader* loader = [RKTestResponseLoader new]; + RKClient* client = [RKTestFactory client]; + client.username = RKAuthenticationTestUsername; client.password = @"INVALID"; [client get:@"/authentication/basic" delegate:loader]; [loader waitForResponse]; - assertThatBool([loader.response isOK], is(equalToBool(NO))); + assertThatBool([loader.response isOK], is(equalToBool(NO))); assertThatInteger([loader.response statusCode], is(equalToInt(0))); - assertThatInteger([loader.failureError code], is(equalToInt(NSURLErrorUserCancelledAuthentication))); + assertThatInteger([loader.error code], is(equalToInt(NSURLErrorUserCancelledAuthentication))); + [loader.response.request cancel]; + [loader release]; } - (void)testShouldAuthenticateViaHTTPAuthDigest { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKClient* client = RKSpecNewClient(); - client.username = RKAuthenticationSpecUsername; - client.password = RKAuthenticationSpecPassword; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKClient* client = [RKTestFactory client]; + client.username = RKAuthenticationTestUsername; + client.password = RKAuthenticationTestPassword; [client get:@"/authentication/digest" delegate:loader]; [loader waitForResponse]; assertThatBool([loader.response isOK], is(equalToBool(YES))); diff --git a/Tests/Logic/Network/RKClientTest.m b/Tests/Logic/Network/RKClientTest.m new file mode 100644 index 0000000000..d746156541 --- /dev/null +++ b/Tests/Logic/Network/RKClientTest.m @@ -0,0 +1,171 @@ +// +// RKClientTest.m +// RestKit +// +// Created by Blake Watters on 1/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "RKTestEnvironment.h" +#import "RKURL.h" + +@interface RKClientTest : RKTestCase +@end + + +@implementation RKClientTest + +- (void)setUp { + [RKTestFactory setUp]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + +- (void)testShouldDetectNetworkStatusWithAHostname { + 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 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; + RKRequest* request = [client requestWithResourcePath:@""]; + assertThatInt(request.cachePolicy, is(equalToInt(RKRequestCachePolicyLoadIfOffline))); +} + +- (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))); +} + +- (void)testShouldLoadPageWithNoContentTypeInformation { + RKClient* client = [RKClient clientWithBaseURLString:@"http://www.semiose.fr"]; + client.defaultHTTPEncoding = NSISOLatin1StringEncoding; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest* request = [client requestWithResourcePath:@"/"]; + request.delegate = loader; + [request send]; + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); + assertThat([loader.response bodyEncodingName], is(nilValue())); + assertThatInteger([loader.response bodyEncoding], is(equalToInteger(NSISOLatin1StringEncoding))); +} + +- (void)testShouldAllowYouToChangeTheBaseURL { + RKClient* client = [RKClient clientWithBaseURLString:@"http://www.google.com"]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle + assertThatBool([client isNetworkReachable], is(equalToBool(YES))); + client.baseURL = [RKURL URLWithString:@"http://www.restkit.org"]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; // Let the runloop cycle + assertThatBool([client isNetworkReachable], is(equalToBool(YES))); + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest* request = [client requestWithResourcePath:@"/"]; + request.delegate = loader; + [request send]; + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); +} + +- (void)testShouldLetYouChangeTheHTTPAuthCredentials { + RKClient *client = [RKTestFactory client]; + client.authenticationType = RKRequestAuthenticationTypeHTTP; + client.username = @"invalid"; + client.password = @"password"; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + [client get:@"/authentication/basic" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(NO))); + assertThat(responseLoader.error, is(notNilValue())); + client.username = @"restkit"; + client.password = @"authentication"; + [client get:@"/authentication/basic" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); +} + +- (void)testShouldSuspendTheQueueOnBaseURLChangeWhenReachabilityHasNotBeenEstablished { + RKClient* client = [RKClient clientWithBaseURLString:@"http://www.google.com"]; + client.baseURL = [RKURL URLWithString:@"http://restkit.org"]; + assertThatBool(client.requestQueue.suspended, is(equalToBool(YES))); +} + +- (void)testShouldNotSuspendTheMainQueueOnBaseURLChangeWhenReachabilityHasBeenEstablished { + RKReachabilityObserver *observer = [RKReachabilityObserver reachabilityObserverForInternet]; + [observer getFlags]; + assertThatBool([observer isReachabilityDetermined], is(equalToBool(YES))); + RKClient *client = [RKClient clientWithBaseURLString:@"http://www.google.com"]; + assertThatBool(client.requestQueue.suspended, is(equalToBool(YES))); + client.reachabilityObserver = observer; + assertThatBool(client.requestQueue.suspended, is(equalToBool(NO))); +} + +- (void)testShouldAllowYouToChangeTheTimeoutInterval { + RKClient* client = [RKClient clientWithBaseURLString:@"http://restkit.org"]; + client.timeoutInterval = 20.0; + RKRequest* request = [client requestWithResourcePath:@""]; + assertThatFloat(request.timeoutInterval, is(equalToFloat(20.0))); +} + +- (void)testShouldPerformAPUTWithParams { + NSLog(@"PENDING ---> FIX ME!!!"); + return; + RKClient* client = [RKClient clientWithBaseURLString:@"http://ohblockhero.appspot.com/api/v1"]; + client.cachePolicy = RKRequestCachePolicyNone; + RKParams *params=[RKParams params]; + [params setValue:@"username" forParam:@"username"]; + [params setValue:@"Dear Daniel" forParam:@"fullName"]; + [params setValue:@"aa@aa.com" forParam:@"email"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [client put:@"/userprofile" params:params delegate:loader]; + STAssertNoThrow([loader waitForResponse], @""); + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); +} + +- (void)testShouldAllowYouToChangeTheCacheTimeoutInterval { + RKClient* client = [RKClient clientWithBaseURLString:@"http://restkit.org"]; + client.cacheTimeoutInterval = 20.0; + RKRequest* request = [client requestWithResourcePath:@""]; + 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 new file mode 100644 index 0000000000..ba88dd12cb --- /dev/null +++ b/Tests/Logic/Network/RKOAuthClientTest.m @@ -0,0 +1,92 @@ +// +// RKOAuthClientTest.m +// RestKit +// +// Created by Rodrigo Garcia on 8/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKPortCheck.h" + +#define RKOAuthClientTestSkipWithoutMongoDB() \ + if (! [self isMongoRunning]) { \ + NSLog(@"!! Skipping OAuth Test: MongoDB not running"); \ + return; \ + } + +@interface RKOAuthClientTest : RKTestCase + +@end + +@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 = @"4fa8182d7184797dd5000002"; + client.callbackURL = @"http://someURL.com"; + [client validateAuthorizationCode]; + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); +} + +- (void)testShouldNotGetAccessToken { + RKOAuthClientTestSkipWithoutMongoDB(); + + RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; + RKOAuthClient *client = RKTestNewOAuthClient(loader); + client.authorizationCode = @"someInvalidAuthorizationCode"; + client.callbackURL = @"http://someURL.com"; + [client validateAuthorizationCode]; + [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 = @"4fa8182d7184797dd5000002"; + client.callbackURL = @"http://someURL.com"; + [client validateAuthorizationCode]; + + RKTestResponseLoader* resourceLoader = [RKTestResponseLoader responseLoader]; + RKClient *requestClient = [RKClient clientWithBaseURLString:[client authorizationURL]]; + requestClient.OAuth2AccessToken = client.accessToken; + requestClient.authenticationType = RKRequestAuthenticationTypeOAuth2; + RKRequest *request = [requestClient requestWithResourcePath:@"/oauth2/pregen/me"]; + request.delegate = resourceLoader; + [request send]; + [resourceLoader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); +} + +@end diff --git a/Specs/Network/RKParamsAttachmentSpec.m b/Tests/Logic/Network/RKParamsAttachmentTest.m similarity index 72% rename from Specs/Network/RKParamsAttachmentSpec.m rename to Tests/Logic/Network/RKParamsAttachmentTest.m index 8fb13b160c..c2639143b1 100644 --- a/Specs/Network/RKParamsAttachmentSpec.m +++ b/Tests/Logic/Network/RKParamsAttachmentTest.m @@ -1,16 +1,16 @@ // -// RKParamsAttachmentSpec.m +// RKParamsAttachmentTest.m // RestKit // // Created by Blake Watters on 10/27/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,26 +18,26 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKParamsAttachment.h" -@interface RKParamsAttachmentSpec : RKSpec { +@interface RKParamsAttachmentTest : RKTestCase { } @end -@implementation RKParamsAttachmentSpec +@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/Specs/Network/RKParamsSpec.m b/Tests/Logic/Network/RKParamsTest.m similarity index 64% rename from Specs/Network/RKParamsSpec.m rename to Tests/Logic/Network/RKParamsTest.m index ff0a7d393b..d29699c3bf 100644 --- a/Specs/Network/RKParamsSpec.m +++ b/Tests/Logic/Network/RKParamsTest.m @@ -1,16 +1,16 @@ // -// RKParamsSpec.m +// RKParamsTest.m // RestKit // // Created by Blake Watters on 6/30/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,21 +18,21 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKParams.h" #import "RKRequest.h" -@interface RKParamsSpec : RKSpec +@interface RKParamsTest : RKTestCase @end -@implementation RKParamsSpec +@implementation RKParamsTest - (void)testShouldNotOverReleaseTheParams { NSDictionary* dictionary = [NSDictionary dictionaryWithObject:@"foo" forKey:@"bar"]; RKParams* params = [[RKParams alloc] initWithDictionary:dictionary]; - NSURL* URL = [NSURL URLWithString:[RKSpecGetBaseURL() stringByAppendingFormat:@"/echo_params"]]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; + NSURL* URL = [NSURL URLWithString:[[RKTestFactory baseURLString] stringByAppendingFormat:@"/echo_params"]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.method = RKRequestMethodPOST; request.params = params; @@ -43,52 +43,48 @@ - (void)testShouldNotOverReleaseTheParams { } - (void)testShouldUploadFilesViaRKParams { - RKClient* client = RKSpecNewClient(); + RKClient* client = [RKTestFactory client]; RKParams* params = [RKParams params]; [params setValue:@"one" forParam:@"value"]; [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"]; - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; [client post:@"/upload" params:params delegate:responseLoader]; [responseLoader waitForResponse]; assertThatInteger(responseLoader.response.statusCode, is(equalToInt(200))); } - (void)testShouldUploadFilesViaRKParamsWithMixedTypes { - NSNumber* idUsuari = [NSNumber numberWithInt:1234]; + NSNumber* idUsuari = [NSNumber numberWithInt:1234]; NSArray* userList = [NSArray arrayWithObjects:@"one", @"two", @"three", nil]; - NSNumber* idTema = [NSNumber numberWithInt:1234]; + 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];; - + RKParams* params = [RKParams params]; - + // Set values [params setValue:idUsuari forParam:@"idUsuariPropietari"]; [params setValue:userList forParam:@"telUser"]; [params setValue:idTema forParam:@"idTema"]; [params setValue:titulo forParam:@"titulo"]; [params setValue:texto forParam:@"texto"]; - + [params setData:data MIMEType:@"image/png" forParam:@"file"]; - + [params setValue:cel forParam:@"cel"]; [params setValue:lon forParam:@"lon"]; [params setValue:lat forParam:@"lat"]; - - RKClient* client = RKSpecNewClient(); - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; + + RKClient* client = [RKTestFactory client]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; [client post:@"/upload" params:params delegate:responseLoader]; [responseLoader waitForResponse]; assertThatInteger(responseLoader.response.statusCode, is(equalToInt(200))); @@ -101,4 +97,21 @@ - (void)testShouldCalculateAnMD5ForTheParams { assertThat(MD5, is(equalTo(@"da7d80084b86aa5022b434e3bf084caf"))); } +- (void)testShouldProperlyCalculateContentLengthForFileUploads { + RKClient* client = [RKTestFactory client]; + RKParams* params = [RKParams params]; + [params setValue:@"one" forParam:@"value"]; + [params setValue:@"two" forParam:@"value"]; + [params setValue:@"three" forParam:@"value"]; + [params setValue:@"four" forParam:@"value"]; + NSData *data = [RKTestFixture dataWithContentsOfFixture:@"blake.png"]; + [params setData:data MIMEType:@"image/png" forParam:@"file"]; + RKRequest *request = [client requestWithResourcePath:@"/upload"]; + [request setMethod:RKRequestMethodPOST]; + request.params = params; + [request prepareURLRequest]; + assertThatInteger([params HTTPHeaderValueForContentLength], is(equalToInt(23166))); + assertThat([[request.URLRequest allHTTPHeaderFields] objectForKey:@"Content-Length"], is(equalTo(@"23166"))); +} + @end diff --git a/Specs/Network/RKRequestQueueSpec.m b/Tests/Logic/Network/RKRequestQueueTest.m similarity index 78% rename from Specs/Network/RKRequestQueueSpec.m rename to Tests/Logic/Network/RKRequestQueueTest.m index 22566b3a32..89c7613f05 100644 --- a/Specs/Network/RKRequestQueueSpec.m +++ b/Tests/Logic/Network/RKRequestQueueTest.m @@ -1,16 +1,16 @@ // -// RKRequestQueueSpec.m +// RKRequestQueueTest.m // RestKit // // Created by Blake Watters on 3/28/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,16 +18,30 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" + +// Expose the request queue's [add|remove]LoadingRequest methods testing purposes... +@interface RKRequestQueue () +- (void)addLoadingRequest:(RKRequest*)request; +- (void)removeLoadingRequest:(RKRequest*)request; +@end -@interface RKRequestQueueSpec : RKSpec { - +@interface RKRequestQueueTest : RKTestCase { + NSAutoreleasePool *_autoreleasePool; } @end -@implementation RKRequestQueueSpec +@implementation RKRequestQueueTest + +- (void)setUp { + _autoreleasePool = [NSAutoreleasePool new]; +} + +- (void)tearDown { + [_autoreleasePool drain]; +} - (void)testShouldBeSuspendedWhenInitialized { RKRequestQueue* queue = [RKRequestQueue new]; @@ -88,7 +102,9 @@ - (void)testShouldInformTheDelegateOnTransitionFromEmptyToProcessing { OCMockObject* delegateMock = [OCMockObject niceMockForProtocol:@protocol(RKRequestQueueDelegate)]; [[delegateMock expect] requestQueueDidBeginLoading:queue]; queue.delegate = (NSObject*) delegateMock; - [queue setValue:[NSNumber numberWithInt:1] forKey:@"loadingCount"]; + NSURL* URL = [RKTestFactory baseURL]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + [queue addLoadingRequest:request]; [delegateMock verify]; [queue release]; } @@ -98,17 +114,19 @@ - (void)testShouldInformTheDelegateOnTransitionFromProcessingToEmpty { OCMockObject* delegateMock = [OCMockObject niceMockForProtocol:@protocol(RKRequestQueueDelegate)]; [[delegateMock expect] requestQueueDidFinishLoading:queue]; queue.delegate = (NSObject*) delegateMock; - [queue setValue:[NSNumber numberWithInt:1] forKey:@"loadingCount"]; - [queue setValue:[NSNumber numberWithInt:0] forKey:@"loadingCount"]; + NSURL* URL = [RKTestFactory baseURL]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + [queue addLoadingRequest:request]; + [queue removeLoadingRequest:request]; [delegateMock verify]; [queue release]; } - (void)testShouldInformTheDelegateOnTransitionFromProcessingToEmptyForQueuesWithASingleRequest { OCMockObject* delegateMock = [OCMockObject niceMockForProtocol:@protocol(RKRequestQueueDelegate)]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/ok-with-delay/0.3", RKSpecGetBaseURL()]; + NSString* url = [NSString stringWithFormat:@"%@/ok-with-delay/0.3", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest * request = [[RKRequest alloc] initWithURL:URL]; request.delegate = loader; @@ -120,6 +138,7 @@ - (void)testShouldInformTheDelegateOnTransitionFromProcessingToEmptyForQueuesWit [queue start]; [loader waitForResponse]; [delegateMock verify]; + [queue release]; } // TODO: These tests cannot pass in the unit testing environment... Need to migrate to an integration @@ -147,19 +166,19 @@ - (void)testShouldInformTheDelegateOnTransitionFromProcessingToEmptyForQueuesWit // //- (void)testShouldJointlyManageTheNetworkActivityIndicator { // [[UIApplication sharedApplication] rk_resetNetworkActivity]; -// RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; +// RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; // loader.timeout = 10; // // RKRequestQueue *queue1 = [RKRequestQueue new]; // queue1.showsNetworkActivityIndicatorWhenBusy = YES; -// NSString* url1 = [NSString stringWithFormat:@"%@/ok-with-delay/2.0", RKSpecGetBaseURL()]; +// NSString* url1 = [NSString stringWithFormat:@"%@/ok-with-delay/2.0", [RKTestFactory baseURL]]; // NSURL* URL1 = [NSURL URLWithString:url1]; // RKRequest * request1 = [[RKRequest alloc] initWithURL:URL1]; // request1.delegate = loader; // // RKRequestQueue *queue2 = [RKRequestQueue new]; // queue2.showsNetworkActivityIndicatorWhenBusy = YES; -// NSString* url2 = [NSString stringWithFormat:@"%@/ok-with-delay/2.0", RKSpecGetBaseURL()]; +// NSString* url2 = [NSString stringWithFormat:@"%@/ok-with-delay/2.0", [RKTestFactory baseURL]]; // NSURL* URL2 = [NSURL URLWithString:url2]; // RKRequest * request2 = [[RKRequest alloc] initWithURL:URL2]; // request2.delegate = loader; @@ -227,13 +246,37 @@ - (void)testShouldReturnNilIfNewRequestQueueWithNameIsCalledForAnExistingName { - (void)testShouldRemoveItemsFromTheQueueWithAnUnmappableResponse { RKRequestQueue *queue = [RKRequestQueue requestQueue]; - RKObjectManager *objectManager = RKSpecNewObjectManager(); - RKSpecResponseLoader *loader = [RKSpecResponseLoader responseLoader]; - RKObjectLoader *objectLoader = [RKObjectLoader loaderWithResourcePath:@"/403" objectManager:objectManager delegate:loader]; + RKObjectManager *objectManager = [RKTestFactory objectManager]; + RKTestResponseLoader *loader = [RKTestResponseLoader responseLoader]; + RKObjectLoader *objectLoader = [objectManager loaderWithResourcePath:@"/403"]; + objectLoader.delegate = loader; [queue addRequest:(RKRequest *)objectLoader]; [queue start]; [loader waitForResponse]; 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/Specs/Network/RKRequestSpec.m b/Tests/Logic/Network/RKRequestTest.m similarity index 53% rename from Specs/Network/RKRequestSpec.m rename to Tests/Logic/Network/RKRequestTest.m index 1b862ab5ef..f039a97ee5 100644 --- a/Specs/Network/RKRequestSpec.m +++ b/Tests/Logic/Network/RKRequestTest.m @@ -1,16 +1,16 @@ // -// RKRequestSpec.m +// RKRequestTest.m // RestKit // // Created by Blake Watters on 1/15/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,7 +18,7 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKRequest.h" #import "RKParams.h" #import "RKResponse.h" @@ -30,48 +30,52 @@ - (void)fireAsynchronousRequest; - (void)shouldDispatchRequest; @end -@interface RKRequestSpec : RKSpec { +@interface RKRequestTest : RKTestCase { int _methodInvocationCounter; } @end -@implementation RKRequestSpec +@implementation RKRequestTest - (void)setUp { + [RKTestFactory setUp]; + // Clear the cache directory - RKSpecClearCacheDirectory(); + [RKTestFactory clearCacheDirectory]; _methodInvocationCounter = 0; } +- (void)tearDown { + [RKTestFactory tearDown]; +} + - (int)incrementMethodInvocationCounter { return _methodInvocationCounter++; } /** * This spec requires the test Sinatra server to be running - * `ruby Specs/server.rb` + * `ruby Tests/server.rb` */ - (void)testShouldSendMultiPartRequests { - NSString* URLString = [NSString stringWithFormat:@"http://127.0.0.1:4567/photo"]; - NSURL* URL = [NSURL URLWithString:URLString]; - RKSpecStubNetworkAvailability(YES); - 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 - (void)testShouldSetURLRequestHTTPBody { - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + NSURL* URL = [NSURL URLWithString:[RKTestFactory baseURLString]]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; NSString* JSON = @"whatever"; NSData* data = [JSON dataUsingEncoding:NSASCIIStringEncoding]; @@ -82,7 +86,7 @@ - (void)testShouldSetURLRequestHTTPBody { } - (void)testShouldSetURLRequestHTTPBodyByString { - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + NSURL* URL = [NSURL URLWithString:[RKTestFactory baseURLString]]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; NSString* JSON = @"whatever"; NSData* data = [JSON dataUsingEncoding:NSASCIIStringEncoding]; @@ -92,25 +96,36 @@ - (void)testShouldSetURLRequestHTTPBodyByString { assertThat(request.HTTPBodyString, equalTo(JSON)); } -- (void)testShouldTimeoutAtInterval { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; +- (void)testShouldTimeoutAtIntervalWhenSentAsynchronously { + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; id loaderMock = [OCMockObject partialMockForObject:loader]; - NSString* url = [NSString stringWithFormat:@"%@/timeout", RKSpecGetBaseURL()]; - NSURL* URL = [NSURL URLWithString:url]; + NSURL* URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/timeout"]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.delegate = loaderMock; request.timeoutInterval = 3.0; [[[loaderMock expect] andForwardToRealObject] request:request didFailLoadWithError:OCMOCK_ANY]; [request sendAsynchronously]; [loaderMock waitForResponse]; - assertThatInt((int)loader.failureError.code, equalToInt(RKRequestConnectionTimeoutError)); + assertThatInt((int)loader.error.code, equalToInt(RKRequestConnectionTimeoutError)); [request release]; } -- (void)testShouldCreateOneTimeoutTimer { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKURL* url = RKSpecGetBaseURL(); - RKRequest* request = [[RKRequest alloc] initWithURL:url]; +- (void)testShouldTimeoutAtIntervalWhenSentSynchronously { + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + id loaderMock = [OCMockObject partialMockForObject:loader]; + NSURL* URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/timeout"]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.delegate = loaderMock; + request.timeoutInterval = 3.0; + [[[loaderMock expect] andForwardToRealObject] request:request didFailLoadWithError:OCMOCK_ANY]; + [request sendSynchronously]; + assertThatInt((int)loader.error.code, equalToInt(RKRequestConnectionTimeoutError)); + [request release]; +} + +- (void)testShouldCreateOneTimeoutTimerWhenSentAsynchronously { + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest* request = [[RKRequest alloc] initWithURL:[RKTestFactory baseURL]]; request.delegate = loader; id requestMock = [OCMockObject partialMockForObject:request]; [[[requestMock expect] andCall:@selector(incrementMethodInvocationCounter) onObject:self] createTimeoutTimer]; @@ -120,14 +135,41 @@ - (void)testShouldCreateOneTimeoutTimer { [request release]; } +- (void)testThatSendingDataInvalidatesTimeoutTimer { + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + loader.timeout = 3.0; + NSURL* URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/timeout"]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.method = RKRequestMethodPOST; + request.delegate = loader; + request.params = [NSDictionary dictionaryWithObject:@"test" forKey:@"test"]; +request.timeoutInterval = 1.0; + [request sendAsynchronously]; + [loader waitForResponse]; + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); + [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 { - RKSpecStubNetworkAvailability(YES); - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; - 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 @@ -141,16 +183,16 @@ - (UIApplication *)sharedApplicationMock { } - (void)stubSharedApplicationWhileExecutingBlock:(void (^)(void))block { - [self swizzleMethod:@selector(sharedApplication) - inClass:[UIApplication class] - withMethod:@selector(sharedApplicationMock) - fromClass:[self class] + [self swizzleMethod:@selector(sharedApplication) + inClass:[UIApplication class] + withMethod:@selector(sharedApplicationMock) + fromClass:[self class] executeBlock:block]; } - (void)testShouldObserveForAppBackgroundTransitionsAndCancelTheRequestWhenBackgroundPolicyIsRKRequestBackgroundPolicyCancel { [self stubSharedApplicationWhileExecutingBlock:^{ - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyCancel; id requestMock = [OCMockObject partialMockForObject:request]; @@ -158,14 +200,14 @@ - (void)testShouldObserveForAppBackgroundTransitionsAndCancelTheRequestWhenBackg [requestMock sendAsynchronously]; [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; [requestMock verify]; - [request release]; }]; } - (void)testShouldInformTheDelegateOfCancelWhenTheRequestWhenBackgroundPolicyIsRKRequestBackgroundPolicyCancel { + [RKTestFactory client]; [self stubSharedApplicationWhileExecutingBlock:^{ - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyCancel; request.delegate = loader; @@ -173,15 +215,28 @@ - (void)testShouldInformTheDelegateOfCancelWhenTheRequestWhenBackgroundPolicyIsR [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; assertThatBool(loader.wasCancelled, is(equalToBool(YES))); [request release]; - }]; + }]; +} + +- (void)testShouldDeallocTheRequestWhenBackgroundPolicyIsRKRequestBackgroundPolicyCancel { + [RKTestFactory client]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; + RKRequest* request = [[RKRequest alloc] initWithURL:URL]; + request.backgroundPolicy = RKRequestBackgroundPolicyCancel; + request.delegate = loader; + [request sendAsynchronously]; + [loader waitForResponse]; + assertThatInteger([request retainCount], is(equalToInteger(1))); + [request release]; } - (void)testShouldPutTheRequestBackOntoTheQueueWhenBackgroundPolicyIsRKRequestBackgroundPolicyRequeue { [self stubSharedApplicationWhileExecutingBlock:^{ RKRequestQueue* queue = [RKRequestQueue new]; queue.suspended = YES; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyRequeue; request.delegate = loader; @@ -195,7 +250,7 @@ - (void)testShouldPutTheRequestBackOntoTheQueueWhenBackgroundPolicyIsRKRequestBa } - (void)testShouldCreateABackgroundTaskWhenBackgroundPolicyIsRKRequestBackgroundPolicyContinue { - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyContinue; [request sendAsynchronously]; @@ -203,77 +258,77 @@ - (void)testShouldCreateABackgroundTaskWhenBackgroundPolicyIsRKRequestBackground } - (void)testShouldSendTheRequestWhenBackgroundPolicyIsNone { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyNone; request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); } - (void)testShouldSendTheRequestWhenBackgroundPolicyIsContinue { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyContinue; request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); } - (void)testShouldSendTheRequestWhenBackgroundPolicyIsCancel { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyCancel; request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); } - (void)testShouldSendTheRequestWhenBackgroundPolicyIsRequeue { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSURL* URL = [NSURL URLWithString:RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSURL* URL = [RKTestFactory baseURL]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.backgroundPolicy = RKRequestBackgroundPolicyRequeue; request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); } #endif -#pragma mark RKRequestCachePolicy Specs +#pragma mark RKRequestCachePolicy Tests - (void)testShouldSendTheRequestWhenTheCachePolicyIsNone { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyNone; request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); } - (void)testShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHeader { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -281,7 +336,7 @@ - (void)testShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHea request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); NSDictionary* headers = [cache headersForRequest:request]; assertThat([headers valueForKey:@"X-RESTKIT-CACHEDATE"], isNot(nilValue())); assertThat([headers valueForKey:@"Etag"], is(equalTo(@"686897696a7c876b7e"))); @@ -289,44 +344,42 @@ - (void)testShouldCacheTheRequestHeadersAndBodyIncludingOurOwnCustomTimestampHea } - (void)testShouldGenerateAUniqueCacheKeyBasedOnTheUrlTheMethodAndTheHTTPBody { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; request.method = RKRequestMethodDELETE; // Don't cache delete. cache key should be nil. assertThat([request cacheKey], is(nilValue())); - + request.method = RKRequestMethodPOST; - assertThat([request cacheKey], isNot(nilValue())); - assertThat([request cacheKey], is(equalTo(@"bb373e6316a78f3f0322aa1e5f5818e2"))); - + assertThat([request cacheKey], is(nilValue())); + request.method = RKRequestMethodPUT; - assertThat([request cacheKey], isNot(nilValue())); - assertThat([request cacheKey], is(equalTo(@"aba9267af702ee12cd49b5a2615df182"))); + assertThat([request cacheKey], is(nilValue())); } - (void)testShouldLoadFromCacheWhenWeRecieveA304 { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -334,14 +387,14 @@ - (void)testShouldLoadFromCacheWhenWeRecieveA304 { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThat([cache etagForRequest:request], is(equalTo(@"686897696a7c876b7e"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -349,27 +402,27 @@ - (void)testShouldLoadFromCacheWhenWeRecieveA304 { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(YES))); } } - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - + NSDate* internalCacheDate1; NSDate* internalCacheDate2; { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -377,7 +430,7 @@ - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThat([cache etagForRequest:request], is(equalTo(@"686897696a7c876b7e"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); @@ -385,8 +438,8 @@ - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { } [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.5]]; { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -394,7 +447,7 @@ - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(YES))); internalCacheDate2 = [cache cacheDateForRequest:request]; @@ -403,18 +456,18 @@ - (void)testShouldUpdateTheInternalCacheDateWhenWeRecieveA304 { } - (void)testShouldLoadFromTheCacheIfThereIsAnError { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - + { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -422,13 +475,13 @@ - (void)testShouldLoadFromTheCacheIfThereIsAnError { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyLoadOnError; @@ -441,19 +494,19 @@ - (void)testShouldLoadFromTheCacheIfThereIsAnError { } - (void)testShouldLoadFromTheCacheIfWeAreWithinTheTimeout { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - - NSString* url = [NSString stringWithFormat:@"%@/disk/cached", RKSpecGetBaseURL()]; + + NSString* url = [NSString stringWithFormat:@"%@/disk/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyTimeout; request.cacheTimeoutInterval = 5; @@ -461,36 +514,36 @@ - (void)testShouldLoadFromTheCacheIfWeAreWithinTheTimeout { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached For 5 Seconds"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyTimeout; request.cacheTimeoutInterval = 5; request.cache = cache; request.delegate = loader; [request sendAsynchronously]; - // Don't wait for a response as this actually returns synchronously. + [loader waitForResponse]; assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached For 5 Seconds"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(YES))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyTimeout; request.cacheTimeoutInterval = 5; request.cache = cache; request.delegate = loader; - [request sendSynchronously]; - // Don't wait for a response as this actually returns synchronously. + [request sendAsynchronously]; + [loader waitForResponse]; assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached For 5 Seconds"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(YES))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyTimeout; request.cacheTimeoutInterval = 0; @@ -498,25 +551,26 @@ - (void)testShouldLoadFromTheCacheIfWeAreWithinTheTimeout { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached For 5 Seconds"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } } - (void)testShouldLoadFromTheCacheIfWeAreOffline { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; - + { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + loader.timeout = 60; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -524,13 +578,14 @@ - (void)testShouldLoadFromTheCacheIfWeAreOffline { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + loader.timeout = 60; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyLoadIfOffline; @@ -546,18 +601,18 @@ - (void)testShouldLoadFromTheCacheIfWeAreOffline { } - (void)testShouldCacheTheStatusCodeMIMETypeAndURL { - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; - NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - - RKRequestCache* cache = [[RKRequestCache alloc] initWithCachePath:cachePath + [[NSURL URLWithString:baseURL] host]]; + NSString* cachePath = [[RKDirectory cachesDirectory] + stringByAppendingPathComponent:cacheDirForClient]; + + RKRequestCache* cache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyPermanently]; [cache invalidateWithStoragePolicy:RKRequestCacheStoragePolicyPermanently]; { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -565,15 +620,15 @@ - (void)testShouldCacheTheStatusCodeMIMETypeAndURL { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThat([loader.response bodyAsString], is(equalTo(@"This Should Get Cached"))); NSLog(@"Headers: %@", [cache headersForRequest:request]); assertThat([cache etagForRequest:request], is(equalTo(@"686897696a7c876b7e"))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(NO))); } { - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - NSString* url = [NSString stringWithFormat:@"%@/etags/cached", RKSpecGetBaseURL()]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + NSString* url = [NSString stringWithFormat:@"%@/etags/cached", [RKTestFactory baseURLString]]; NSURL* URL = [NSURL URLWithString:url]; RKRequest* request = [[RKRequest alloc] initWithURL:URL]; request.cachePolicy = RKRequestCachePolicyEtag; @@ -581,7 +636,7 @@ - (void)testShouldCacheTheStatusCodeMIMETypeAndURL { request.delegate = loader; [request sendAsynchronously]; [loader waitForResponse]; - assertThatBool([loader success], is(equalToBool(YES))); + assertThatBool([loader wasSuccessful], is(equalToBool(YES))); assertThatBool([loader.response wasLoadedFromCache], is(equalToBool(YES))); assertThatInteger(loader.response.statusCode, is(equalToInt(200))); assertThat(loader.response.MIMEType, is(equalTo(@"text/html"))); @@ -591,14 +646,13 @@ - (void)testShouldCacheTheStatusCodeMIMETypeAndURL { - (void)testShouldPostSimpleKeyValuesViaRKParams { RKParams* params = [RKParams params]; - + [params setValue: @"hello" forParam:@"username"]; [params setValue: @"password" forParam:@"password"]; - - RKClient* client = RKSpecNewClient(); + + RKClient* client = [RKTestFactory client]; client.cachePolicy = RKRequestCachePolicyNone; - RKSpecStubNetworkAvailability(YES); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; loader.timeout = 20; [client post:@"/echo_params" params:params delegate:loader]; [loader waitForResponse]; @@ -606,10 +660,9 @@ - (void)testShouldPostSimpleKeyValuesViaRKParams { } - (void)testShouldSetAnEmptyContentBodyWhenParamsIsNil { - RKClient* client = RKSpecNewClient(); + RKClient* client = [RKTestFactory client]; client.cachePolicy = RKRequestCachePolicyNone; - RKSpecStubNetworkAvailability(YES); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; loader.timeout = 20; RKRequest* request = [client get:@"/echo_params" delegate:loader]; [loader waitForResponse]; @@ -617,22 +670,21 @@ - (void)testShouldSetAnEmptyContentBodyWhenParamsIsNil { } - (void)testShouldSetAnEmptyContentBodyWhenQueryParamsIsAnEmptyDictionary { - RKClient* client = RKSpecNewClient(); + RKClient* client = [RKTestFactory client]; client.cachePolicy = RKRequestCachePolicyNone; - RKSpecStubNetworkAvailability(YES); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; loader.timeout = 20; - RKRequest* request = [client get:@"/echo_params" queryParams:[NSDictionary dictionary] delegate:loader]; + RKRequest* request = [client get:@"/echo_params" queryParameters:[NSDictionary dictionary] delegate:loader]; [loader waitForResponse]; assertThat([request.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); } - (void)testShouldPUTWithParams { - RKClient* client = RKSpecNewClient(); - RKParams *params = [RKParams params]; - [params setValue:@"ddss" forParam:@"username"]; + RKClient* client = [RKTestFactory client]; + RKParams *params = [RKParams params]; + [params setValue:@"ddss" forParam:@"username"]; [params setValue:@"aaaa@aa.com" forParam:@"email"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; [client put:@"/ping" params:params delegate:loader]; [loader waitForResponse]; assertThat([loader.response bodyAsString], is(equalTo(@"{\"username\":\"ddss\",\"email\":\"aaaa@aa.com\"}"))); @@ -640,21 +692,21 @@ - (void)testShouldPUTWithParams { - (void)testShouldAllowYouToChangeTheURL { NSURL* URL = [NSURL URLWithString:@"http://restkit.org/monkey"]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:self]; + RKRequest* request = [RKRequest requestWithURL:URL]; request.URL = [NSURL URLWithString:@"http://restkit.org/gorilla"]; assertThat([request.URL absoluteString], is(equalTo(@"http://restkit.org/gorilla"))); } - (void)testShouldAllowYouToChangeTheResourcePath { - RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/monkey"]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:self]; + RKURL* URL = [[RKURL URLWithString:@"http://restkit.org"] URLByAppendingResourcePath:@"/monkey"]; + RKRequest* request = [RKRequest requestWithURL:URL]; request.resourcePath = @"/gorilla"; assertThat(request.resourcePath, is(equalTo(@"/gorilla"))); } -- (void)testShouldRaiseAnExceptionWhenAttemptingToMutateResourcePathOnAnNSURL { +- (void)testShouldNotRaiseAnExceptionWhenAttemptingToMutateResourcePathOnAnNSURL { NSURL* URL = [NSURL URLWithString:@"http://restkit.org/monkey"]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:self]; + RKRequest* request = [RKRequest requestWithURL:URL]; NSException* exception = nil; @try { request.resourcePath = @"/gorilla"; @@ -663,27 +715,25 @@ - (void)testShouldRaiseAnExceptionWhenAttemptingToMutateResourcePathOnAnNSURL { exception = e; } @finally { - assertThat(exception, is(notNilValue())); + assertThat(exception, is(nilValue())); } } - (void)testShouldOptionallySkipSSLValidation { - RKClient* client = RKSpecNewClient(); - client.disableCertificateValidation = YES; - NSURL* URL = [NSURL URLWithString:@"https://blakewatters.com/"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:loader]; - [request send]; - [loader waitForResponse]; - assertThatBool([loader.response isOK], is(equalToBool(YES))); + NSURL *URL = [NSURL URLWithString:@"https://blakewatters.com/"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + request.disableCertificateValidation = YES; + RKResponse *response = [request sendSynchronously]; + assertThatBool([response isOK], is(equalToBool(YES))); } - (void)testShouldNotAddANonZeroContentLengthHeaderIfParamsIsSetAndThisIsAGETRequest { - RKClient* client = RKSpecNewClient(); + RKClient* client = [RKTestFactory client]; client.disableCertificateValidation = YES; NSURL* URL = [NSURL URLWithString:@"https://blakewatters.com/"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:loader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest* request = [RKRequest requestWithURL:URL]; + request.delegate = loader; request.params = [NSDictionary dictionaryWithObject:@"foo" forKey:@"bar"]; [request send]; [loader waitForResponse]; @@ -691,11 +741,12 @@ - (void)testShouldNotAddANonZeroContentLengthHeaderIfParamsIsSetAndThisIsAGETReq } - (void)testShouldNotAddANonZeroContentLengthHeaderIfParamsIsSetAndThisIsAHEADRequest { - RKClient* client = RKSpecNewClient(); + RKClient* client = [RKTestFactory client]; client.disableCertificateValidation = YES; NSURL* URL = [NSURL URLWithString:@"https://blakewatters.com/"]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - RKRequest* request = [RKRequest requestWithURL:URL delegate:loader]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + RKRequest* request = [RKRequest requestWithURL:URL]; + request.delegate = loader; request.method = RKRequestMethodHEAD; request.params = [NSDictionary dictionaryWithObject:@"foo" forKey:@"bar"]; [request send]; @@ -703,19 +754,47 @@ - (void)testShouldNotAddANonZeroContentLengthHeaderIfParamsIsSetAndThisIsAHEADRe assertThat([request.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); } -// TODO: Move to RKRequestCacheSpec +- (void)testShouldLetYouHandleResponsesWithABlock { + RKURL *URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/ping"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + request.delegate = responseLoader; + __block BOOL blockInvoked = NO; + request.onDidLoadResponse = ^ (RKResponse *response) { + blockInvoked = YES; + }; + [request sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatBool(blockInvoked, is(equalToBool(YES))); +} + +- (void)testShouldLetYouHandleErrorsWithABlock { + RKURL *URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/fail"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + request.delegate = responseLoader; + __block BOOL blockInvoked = NO; + request.onDidLoadResponse = ^ (RKResponse *response) { + blockInvoked = YES; + }; + [request sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatBool(blockInvoked, is(equalToBool(YES))); +} + +// TODO: Move to RKRequestCacheTest - (void)testShouldReturnACachePathWhenTheRequestIsUsingRKParams { RKParams *params = [RKParams params]; [params setValue:@"foo" forParam:@"bar"]; NSURL *URL = [NSURL URLWithString:@"http://restkit.org/"]; - RKRequest *request = [RKRequest requestWithURL:URL delegate:nil]; + RKRequest *request = [RKRequest requestWithURL:URL]; request.params = params; - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; + [[NSURL URLWithString:baseURL] host]]; NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache *requestCache = [[RKRequestCache alloc] initWithCachePath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache *requestCache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; NSString *requestCachePath = [requestCache pathForRequest:request]; NSArray *pathComponents = [requestCachePath pathComponents]; NSString *cacheFile = [NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange([pathComponents count] - 2, 2)]]; @@ -726,20 +805,20 @@ - (void)testShouldReturnNilForCachePathWhenTheRequestIsADELETE { RKParams *params = [RKParams params]; [params setValue:@"foo" forParam:@"bar"]; NSURL *URL = [NSURL URLWithString:@"http://restkit.org/"]; - RKRequest *request = [RKRequest requestWithURL:URL delegate:nil]; + RKRequest *request = [RKRequest requestWithURL:URL]; request.method = RKRequestMethodDELETE; - NSString* baseURL = RKSpecGetBaseURL(); + NSString* baseURL = [RKTestFactory baseURLString]; NSString* cacheDirForClient = [NSString stringWithFormat:@"RKClientRequestCache-%@", - [[NSURL URLWithString:baseURL] host]]; + [[NSURL URLWithString:baseURL] host]]; NSString* cachePath = [[RKDirectory cachesDirectory] - stringByAppendingPathComponent:cacheDirForClient]; - RKRequestCache *requestCache = [[RKRequestCache alloc] initWithCachePath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; + stringByAppendingPathComponent:cacheDirForClient]; + RKRequestCache *requestCache = [[RKRequestCache alloc] initWithPath:cachePath storagePolicy:RKRequestCacheStoragePolicyForDurationOfSession]; NSString *requestCachePath = [requestCache pathForRequest:request]; assertThat(requestCachePath, is(nilValue())); } - (void)testShouldBuildAProperAuthorizationHeaderForOAuth1 { - RKRequest *request = [RKRequest requestWithURL:[RKURL URLWithString:@"http://restkit.org/this?that=foo&bar=word"] delegate:nil]; + RKRequest *request = [RKRequest requestWithURL:[RKURL URLWithString:@"http://restkit.org/this?that=foo&bar=word"]]; request.authenticationType = RKRequestAuthenticationTypeOAuth1; request.OAuth1AccessToken = @"12345"; request.OAuth1AccessTokenSecret = @"monkey"; @@ -749,4 +828,174 @@ - (void)testShouldBuildAProperAuthorizationHeaderForOAuth1 { NSString *authorization = [request.URLRequest valueForHTTPHeaderField:@"Authorization"]; 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]; + RKRequest *request = [RKRequest requestWithURL:URL]; + __block RKResponse *blockResponse = nil; + request.onDidLoadResponse = ^ (RKResponse *response) { + blockResponse = response; + }; + request.delegate = loader; + [request sendAsynchronously]; + [loader waitForResponse]; + assertThat(blockResponse, is(notNilValue())); +} + +- (void)testOnDidFailLoadWithErrorBlockInvocation { + RKURL *URL = [[RKTestFactory baseURL] URLByAppendingResourcePath:@"/503"]; + RKRequest *request = [RKRequest requestWithURL:URL]; + __block NSError *blockError = nil; + request.onDidFailLoadWithError = ^ (NSError *error) { + blockError = error; + }; + NSError *expectedError = [NSError errorWithDomain:@"Test" code:1234 userInfo:nil]; + [request didFailLoadWithError:expectedError]; + assertThat(blockError, is(notNilValue())); +} + +- (void)testShouldBuildAProperRequestWhenSettingBodyByMIMEType { + RKClient* client = [RKTestFactory client]; + NSDictionary *bodyParams = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:10], @"number", + @"JSON String", @"string", + nil]; + RKRequest *request = [client requestWithResourcePath:@"/upload"]; + [request setMethod:RKRequestMethodPOST]; + [request setBody:bodyParams forMIMEType:RKMIMETypeJSON]; + [request prepareURLRequest]; + assertThat(request.HTTPBodyString, is(equalTo(@"{\"number\":10,\"string\":\"JSON String\"}"))); +} + +- (void)testThatGETRequestsAreConsideredCacheable +{ + RKRequest *request = [RKRequest new]; + request.method = RKRequestMethodGET; + assertThatBool([request isCacheable], is(equalToBool(YES))); +} + +- (void)testThatPOSTRequestsAreNotConsideredCacheable +{ + RKRequest *request = [RKRequest new]; + request.method = RKRequestMethodPOST; + assertThatBool([request isCacheable], is(equalToBool(NO))); +} + +- (void)testThatPUTRequestsAreNotConsideredCacheable +{ + RKRequest *request = [RKRequest new]; + request.method = RKRequestMethodPUT; + assertThatBool([request isCacheable], is(equalToBool(NO))); +} + +- (void)testThatDELETERequestsAreNotConsideredCacheable +{ + RKRequest *request = [RKRequest new]; + request.method = RKRequestMethodDELETE; + 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 new file mode 100644 index 0000000000..0ba76ba11e --- /dev/null +++ b/Tests/Logic/Network/RKResponseTest.m @@ -0,0 +1,311 @@ +// +// RKResponseTest.m +// RestKit +// +// Created by Blake Watters on 1/15/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKResponse.h" + +@interface RKResponseTest : RKTestCase { + RKResponse* _response; +} + +@end + +@implementation RKResponseTest + +- (void)setUp { + _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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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))); +} + +- (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"))); +} + +// 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"))); +} + +- (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"))); +} + +- (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))); +} + +- (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))); +} + +- (void)testShouldReturnParseErrorsWhenParsedBodyFails { + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + 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]; + assertThat(object, is(nilValue())); + assertThat(error, isNot(nilValue())); + assertThat([error localizedDescription], is(equalTo(@"Unexpected token, wanted '{', '}', '[', ']', ',', ':', 'true', 'false', 'null', '\"STRING\"', 'NUMBER'."))); +} + +- (void)testShouldNotCrashOnFailureToParseBody { + RKResponse *response = [[RKResponse new] autorelease]; + id mockResponse = [OCMockObject partialMockForObject:response]; + [[[mockResponse stub] andReturn:@"test/fake"] MIMEType]; + [[[mockResponse stub] andReturn:@"whatever"] bodyAsString]; + NSError *error = nil; + id parsedResponse = [mockResponse parsedBody:&error]; + assertThat(parsedResponse, is(nilValue())); +} + +- (void)testShouldNotCrashWhenParserReturnsNilWithoutAnError { + RKResponse* response = [[[RKResponse alloc] init] autorelease]; + 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]]; + [[[mockRegistry expect] andReturn:mockParser] parserForMIMEType:RKMIMETypeJSON]; + NSError* error = nil; + [[[mockParser expect] andReturn:nil] objectFromString:@"" error:[OCMArg setTo:error]]; + id object = [mockResponse parsedBody:&error]; + [mockRegistry verify]; + [mockParser verify]; + [RKParserRegistry setSharedRegistry:nil]; + assertThat(object, is(nilValue())); + assertThat(error, is(nilValue())); +} + +- (void)testLoadingNonUTF8Charset { + RKClient* client = [RKTestFactory client]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [client get:@"/encoding" delegate:loader]; + [loader waitForResponse]; + assertThat([loader.response bodyEncodingName], is(equalTo(@"us-ascii"))); + 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/Network/RKURLTest.m b/Tests/Logic/Network/RKURLTest.m new file mode 100644 index 0000000000..c6f9531d96 --- /dev/null +++ b/Tests/Logic/Network/RKURLTest.m @@ -0,0 +1,215 @@ +// +// RKURLTest.m +// RestKit +// +// Created by Blake Watters on 6/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKURL.h" +#import "NSURL+RKAdditions.h" + +@interface RKURLTest : RKTestCase +@end + +@implementation RKURLTest + +- (void)testShouldNotExplodeBecauseOfUnicodeCharacters { + NSException* failed = nil; + @try { + [RKURL URLWithBaseURLString:@"http://test.com" resourcePath:@"/places.json?category=Automóviles"]; + } + @catch (NSException *exception) { + failed = exception; + } + @finally { + NSAssert((failed == nil), @"No exception should be generated by creating URL with Unicode chars"); + } +} + +- (void)testShouldEscapeQueryParameters { + NSDictionary* queryParams = [NSDictionary dictionaryWithObjectsAndKeys:@"What is your #1 e-mail?", @"question", @"john+restkit@gmail.com", @"answer", nil]; + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParameters:queryParams]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test?answer=john%2Brestkit%40gmail.com&question=What%20is%20your%20%231%20e-mail%3F"))); +} + +- (void)testShouldHandleNilQueryParameters { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); +} + +- (void)testShouldHandleEmptyQueryParameters { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test" queryParameters:[NSDictionary dictionary]]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); +} + +- (void)testShouldHandleResourcePathWithoutLeadingSlash { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"test" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test"))); +} + +- (void)testShouldHandleEmptyResourcePath { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org"))); +} + +- (void)testShouldHandleBaseURLsWithAPath { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org/this" resourcePath:@"/test" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/this/test"))); +} + +- (void)testShouldSimplifyURLsWithSeveralSlashes { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org//this//" resourcePath:@"/test" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/this/test"))); +} + +- (void)testShouldPreserveTrailingSlash { + RKURL* URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/test/" queryParameters:nil]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/test/"))); +} + +- (void)testShouldReturnTheMIMETypeForURL { + NSURL *URL = [NSURL URLWithString:@"http://restkit.org/path/to/resource.xml"]; + assertThat([URL MIMETypeForPathExtension], is(equalTo(@"application/xml"))); +} + +- (void)testInitializationFromStringIsEqualToAbsoluteString { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org"]; + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org"))); +} + +- (void)testInitializationFromStringHasSelfAsBaseURL { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org"]; + assertThat([[URL baseURL] absoluteString], is(equalTo(@"http://restkit.org"))); +} + +- (void)testInitializationFromStringHasNilResourcePath { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org"]; + assertThat([URL resourcePath], is(nilValue())); +} + +- (void)testInitializationFromStringHasNilQueryParameters { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org"]; + assertThat([URL query], is(nilValue())); + assertThat([URL queryParameters], is(nilValue())); +} + +- (void)testInitializationFromStringIncludingQueryParameters { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org/foo?bar=1&this=that"]; + assertThat([URL queryParameters], hasEntries(@"bar", equalTo(@"1"), @"this", equalTo(@"that"), nil)); +} + +- (void)testInitializationFromURLandResourcePathIncludingQueryParameters { + NSString *resourcePath = @"/bar?another=option"; + RKURL *URL = [RKURL URLWithBaseURLString:@"http://restkit.org/foo?bar=1&this=that" resourcePath:resourcePath]; +#if TARGET_OS_IPHONE + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/foo/bar?bar=1&this=that&another=option"))); +#else + assertThat([URL absoluteString], is(equalTo(@"http://restkit.org/foo/bar?bar=1&another=option&this=that"))); +#endif + assertThat([URL queryParameters], hasEntries(@"bar", equalTo(@"1"), + @"this", equalTo(@"that"), + @"another", equalTo(@"option"), nil)); +} + +- (void)testInitializationFromNSURL { + NSURL *URL = [NSURL URLWithString:@"http://restkit.org"]; + RKURL *rkURL = [RKURL URLWithBaseURL:URL]; + assertThat(URL, is(equalTo(rkURL))); +} + +- (void)testInitializationFromRKURL { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org"]; + RKURL *rkURL = [RKURL URLWithBaseURL:URL]; + assertThat(URL, is(equalTo(rkURL))); +} + +- (void)testInitializationFromNSURLandAppendingOfResourcePath { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org/"]; + RKURL *rkURL = [RKURL URLWithBaseURL:URL resourcePath:@"/entries"]; + assertThat([rkURL absoluteString], equalTo(@"http://restkit.org/entries")); +} + +- (void)testMergingOfAdditionalQueryParameters { + NSURL *URL = [NSURL URLWithString:@"http://restkit.org/search?title=Hacking"]; + NSDictionary *params = [NSDictionary dictionaryWithObject:@"Computers" forKey:@"genre"]; + RKURL *newURL = [RKURL URLWithBaseURL:URL resourcePath:nil queryParameters:params]; + assertThat([newURL queryParameters], hasEntries(@"title", equalTo(@"Hacking"), @"genre", equalTo(@"Computers"), nil)); +} + +- (void)testReplacementOfExistingResourcePath { + RKURL *URL = [RKURL URLWithBaseURLString:@"http://restkit.org/" resourcePath:@"/articles"]; + RKURL *newURL = [URL URLByReplacingResourcePath:@"/files"]; + assertThat([newURL absoluteString], equalTo(@"http://restkit.org/files")); + assertThat([newURL resourcePath], equalTo(@"/files")); +} + +- (void)testReplacementOfNilResourcePath { + RKURL *URL = [RKURL URLWithString:@"http://restkit.org/whatever"]; + assertThat([URL resourcePath], is(nilValue())); + RKURL *newURL = [URL URLByReplacingResourcePath:@"/works"]; + assertThat([newURL resourcePath], is(equalTo(@"/works"))); + assertThat([newURL absoluteString], is(equalTo(@"http://restkit.org/whatever/works"))); +} + +- (void)testInterpolationOfResourcePath { + RKURL *URL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:@"/paginate?page=:page&perPage=:per_page"]; + NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"1", @"page", @"25", @"per_page", nil]; + RKURL *interpolatedURL = [URL URLByInterpolatingResourcePathWithObject:dictionary]; + assertThat([interpolatedURL resourcePath], is(equalTo(@"/paginate?page=1&perPage=25"))); + + NSDictionary *queryParams = [NSDictionary dictionaryWithObjectsAndKeys: + @"25", @"perPage", + @"1", @"page", + nil]; + assertThat([interpolatedURL resourcePath], is(equalTo(@"/paginate?page=1&perPage=25"))); + assertThat([interpolatedURL queryParameters], is(equalTo(queryParams))); +} + +- (void)testShouldProperlyHandleLongURLParameterValues { + NSString *longResourcePath = @""; + for (NSUInteger index = 0; index < 1050; index++) { + longResourcePath = [longResourcePath stringByAppendingString:@"X"]; + } + assertThatInteger([longResourcePath length], is(equalToInt(1050))); + + RKURL *URL = [RKURL URLWithBaseURLString:[RKTestFactory baseURLString] + resourcePath:longResourcePath]; + assertThat([URL absoluteString], is(equalTo([NSString stringWithFormat:@"%@/%@", [RKTestFactory baseURLString], longResourcePath]))); +} + +- (void)testThatPathIsPreservedWhenURLIsConstructedFromAnotherRKURL +{ + RKURL *URL = [RKURL URLWithBaseURL:[RKTestFactory baseURL] resourcePath:@"/this/and/that"]; + RKURL *newURL = [URL URLByAppendingResourcePath:@"/the/other/thing" queryParameters:[NSDictionary dictionaryWithObject:@"up" forKey:@"word"]]; + assertThat([newURL absoluteString], is(equalTo(@"http://127.0.0.1:4567/this/and/that/the/other/thing?word=up"))); +} + +- (void)testThatResourcePathIsPreservedWhenURLIsConstructedFromAnotherRKURL +{ + RKURL *URL = [RKURL URLWithBaseURL:[RKTestFactory baseURL] resourcePath:@"/this/and/that"]; + RKURL *newURL = [URL URLByAppendingResourcePath:@"/the/other/thing" queryParameters:[NSDictionary dictionaryWithObject:@"up" forKey:@"word"]]; + assertThat([newURL resourcePath], is(equalTo(@"/the/other/thing"))); +} + +- (void)testThatPathAndQueryParamsArePreservedWhenURLIsConstructedFromAnotherRKURL +{ + RKURL *URL = [RKURL URLWithBaseURL:[RKTestFactory baseURL] resourcePath:@"/this/and/that" queryParameters:[NSDictionary dictionaryWithObject:@"who" forKey:@"where"]]; + RKURL *newURL = [URL URLByAppendingResourcePath:@"/the/other/thing" queryParameters:[NSDictionary dictionaryWithObject:@"up" forKey:@"word"]]; + assertThat([newURL absoluteString], is(equalTo(@"http://127.0.0.1:4567/this/and/that/the/other/thing?where=who&word=up"))); +} + +@end diff --git a/Specs/ObjectMapping/RKDynamicObjectMappingSpec.m b/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m similarity index 75% rename from Specs/ObjectMapping/RKDynamicObjectMappingSpec.m rename to Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m index d0c58ab1d6..ffe6a0c97d 100644 --- a/Specs/ObjectMapping/RKDynamicObjectMappingSpec.m +++ b/Tests/Logic/ObjectMapping/RKDynamicObjectMappingTest.m @@ -1,16 +1,16 @@ // -// RKDynamicObjectMappingSpec.m +// RKDynamicObjectMappingTest.m // RestKit // // Created by Blake Watters on 7/28/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,48 +18,48 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKDynamicObjectMapping.h" #import "RKDynamicMappingModels.h" -@interface RKDynamicObjectMappingSpec : RKSpec +@interface RKDynamicObjectMappingTest : RKTestCase @end -@implementation RKDynamicObjectMappingSpec +@implementation RKDynamicObjectMappingTest - (void)testShouldPickTheAppropriateMappingBasedOnAnAttributeValue { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; - RKObjectMapping* girlMapping = [RKObjectMapping mappingForClass:[Girl class] block:^(RKObjectMapping* mapping) { + RKObjectMapping* girlMapping = [RKObjectMapping mappingForClass:[Girl class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; - RKObjectMapping* boyMapping = [RKObjectMapping mappingForClass:[Boy class] block:^(RKObjectMapping* mapping) { + RKObjectMapping* boyMapping = [RKObjectMapping mappingForClass:[Boy class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; - RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"girl.json")]; + RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"boy.json")]; + mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); } - (void)testShouldMatchOnAnNSNumberAttributeValue { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; - RKObjectMapping* girlMapping = [RKObjectMapping mappingForClass:[Girl class] block:^(RKObjectMapping* mapping) { + RKObjectMapping* girlMapping = [RKObjectMapping mappingForClass:[Girl class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; - RKObjectMapping* boyMapping = [RKObjectMapping mappingForClass:[Boy class] block:^(RKObjectMapping* mapping) { + RKObjectMapping* boyMapping = [RKObjectMapping mappingForClass:[Boy class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"numeric_type" isEqualTo:[NSNumber numberWithInt:0]]; [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"numeric_type" isEqualTo:[NSNumber numberWithInt:1]]; - RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"girl.json")]; + RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"boy.json")]; + mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); } @@ -67,10 +67,10 @@ - (void)testShouldMatchOnAnNSNumberAttributeValue { - (void)testShouldPickTheAppropriateMappingBasedOnDelegateCallback { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; dynamicMapping.delegate = self; - RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"girl.json")]; + RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"boy.json")]; + mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); } @@ -79,21 +79,21 @@ - (void)testShouldPickTheAppropriateMappingBasedOnBlockDelegateCallback { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping* (id data) { if ([[data valueForKey:@"type"] isEqualToString:@"Girl"]) { - return [RKObjectMapping mappingForClass:[Girl class] block:^(RKObjectMapping* mapping) { + return [RKObjectMapping mappingForClass:[Girl class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; } else if ([[data valueForKey:@"type"] isEqualToString:@"Boy"]) { - return [RKObjectMapping mappingForClass:[Boy class] block:^(RKObjectMapping* mapping) { + return [RKObjectMapping mappingForClass:[Boy class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; } - + return nil; }; - RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"girl.json")]; + RKObjectMapping* mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Girl"))); - mapping = [dynamicMapping objectMappingForDictionary:RKSpecParseFixture(@"boy.json")]; + mapping = [dynamicMapping objectMappingForDictionary:[RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]]; assertThat(mapping, is(notNilValue())); assertThat(NSStringFromClass(mapping.objectClass), is(equalTo(@"Boy"))); } @@ -116,15 +116,15 @@ - (void)testShouldFailAnAssertionWhenInvokedWithSomethingOtherThanADictionary { - (RKObjectMapping*)objectMappingForData:(id)data { if ([[data valueForKey:@"type"] isEqualToString:@"Girl"]) { - return [RKObjectMapping mappingForClass:[Girl class] block:^(RKObjectMapping* mapping) { + return [RKObjectMapping mappingForClass:[Girl class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; } else if ([[data valueForKey:@"type"] isEqualToString:@"Boy"]) { - return [RKObjectMapping mappingForClass:[Boy class] block:^(RKObjectMapping* mapping) { + return [RKObjectMapping mappingForClass:[Boy class] usingBlock:^(RKObjectMapping* mapping) { [mapping mapAttributes:@"name", nil]; }]; } - + return nil; } diff --git a/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m b/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m new file mode 100644 index 0000000000..9c39c19a8d --- /dev/null +++ b/Tests/Logic/ObjectMapping/RKObjectLoaderTest.m @@ -0,0 +1,767 @@ +// +// RKObjectLoaderTest.m +// RestKit +// +// Created by Blake Watters on 4/27/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKObjectMappingProvider.h" +#import "RKErrorMessage.h" +#import "RKJSONParserJSONKit.h" + +// Models +#import "RKObjectLoaderTestResultModel.h" + +@interface RKTestComplexUser : NSObject { + NSNumber* _userID; + NSString* _firstname; + NSString* _lastname; + NSString* _email; + NSString* _phone; +} + +@property (nonatomic, retain) NSNumber* userID; +@property (nonatomic, retain) NSString* firstname; +@property (nonatomic, retain) NSString* lastname; +@property (nonatomic, retain) NSString* email; +@property (nonatomic, retain) NSString* phone; + +@end + +@implementation RKTestComplexUser + +@synthesize userID = _userID; +@synthesize firstname = _firstname; +@synthesize lastname = _lastname; +@synthesize phone = _phone; +@synthesize email = _email; + +- (void)willSendWithObjectLoader:(RKObjectLoader *)objectLoader { + return; +} + +@end + +@interface RKTestResponseLoaderWithWillMapData : RKTestResponseLoader { + id _mappableData; +} + +@property (nonatomic, readonly) id mappableData; + +@end + +@implementation RKTestResponseLoaderWithWillMapData + +@synthesize mappableData = _mappableData; + +- (void)dealloc { + [_mappableData release]; + [super dealloc]; +} + +- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData { + [*mappableData setValue:@"monkey!" forKey:@"newKey"]; + _mappableData = [*mappableData retain]; +} + +@end + +///////////////////////////////////////////////////////////////////////////// + +@interface RKObjectLoaderTest : RKTestCase { + +} + +@end + +@implementation RKObjectLoaderTest + +- (void)setUp { + [RKTestFactory setUp]; +} + +- (void)tearDown { + [RKTestFactory tearDown]; +} + +- (RKObjectMappingProvider*)providerForComplexUser { + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [userMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"firstname" toKeyPath:@"firstname"]]; + [provider setMapping:userMapping forKeyPath:@"data.STUser"]; + return provider; +} + +- (RKObjectMappingProvider*)errorMappingProvider { + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; + RKObjectMapping* errorMapping = [RKObjectMapping mappingForClass:[RKErrorMessage class]]; + [errorMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"" toKeyPath:@"errorMessage"]]; + errorMapping.rootKeyPath = @"errors"; + provider.errorMapping = errorMapping; + return provider; +} + +- (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; + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + + assertThat(responseLoader.error, isNot(nilValue())); + assertThat([responseLoader.error localizedDescription], is(equalTo(@"error1, error2"))); + + NSArray* objects = [[responseLoader.error userInfo] objectForKey:RKObjectMapperErrorObjectsKey]; + RKErrorMessage* error1 = [objects objectAtIndex:0]; + RKErrorMessage* error2 = [objects lastObject]; + + assertThat(error1.errorMessage, is(equalTo(@"error1"))); + assertThat(error2.errorMessage, is(equalTo(@"error2"))); +} + +- (void)testShouldNotCrashWhenLoadingAnErrorResponseWithAnUnmappableMIMEType { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [objectManager loadObjectsAtResourcePath:@"/404" delegate:loader]; + [loader waitForResponse]; + assertThatBool(loader.loadedUnexpectedResponse, is(equalToBool(YES))); +} + +#pragma mark - Complex JSON + +- (void)testShouldLoadAComplexUserObjectWithTargetObject { + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + NSString *authString = [NSString stringWithFormat:@"TRUEREST username=%@&password=%@&apikey=123456&class=iphone", @"username", @"password"]; + [objectLoader.URLRequest addValue:authString forHTTPHeaderField:@"Authorization"]; + objectLoader.method = RKRequestMethodGET; + objectLoader.targetObject = user; + objectLoader.mappingProvider = [self providerForComplexUser]; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +- (void)testShouldLoadAComplexUserObjectWithoutTargetObject { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatUnsignedInteger([responseLoader.objects count], is(equalToInt(1))); + RKTestComplexUser* user = [responseLoader.objects lastObject]; + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +- (void)testShouldLoadAComplexUserObjectUsingRegisteredKeyPath { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThatUnsignedInteger([responseLoader.objects count], is(equalToInt(1))); + RKTestComplexUser* user = [responseLoader.objects lastObject]; + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +#pragma mark - willSendWithObjectLoader: + +- (void)testShouldInvokeWillSendWithObjectLoaderOnSend { + RKObjectManager *objectManager = [RKTestFactory objectManager]; + RKTestComplexUser *user = [[RKTestComplexUser new] autorelease]; + id mockObject = [OCMockObject partialMockForObject:user]; + + // Explicitly init so we don't get a managed object loader... + RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader *objectLoader = [[RKObjectLoader alloc] initWithURL:[objectManager.baseURL URLByAppendingResourcePath:@"/200"] mappingProvider:[self providerForComplexUser]]; + objectLoader.configurationDelegate = objectManager; + objectLoader.sourceObject = mockObject; + objectLoader.delegate = responseLoader; + [[mockObject expect] willSendWithObjectLoader:objectLoader]; + [objectLoader send]; + [responseLoader waitForResponse]; + [mockObject verify]; +} + +- (void)testShouldInvokeWillSendWithObjectLoaderOnSendAsynchronously { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + [objectManager setMappingProvider:[self providerForComplexUser]]; + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + id mockObject = [OCMockObject partialMockForObject:user]; + + // Explicitly init so we don't get a managed object loader... + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader *objectLoader = [RKObjectLoader loaderWithURL:[objectManager.baseURL URLByAppendingResourcePath:@"/200"] mappingProvider:objectManager.mappingProvider]; + objectLoader.delegate = responseLoader; + objectLoader.sourceObject = mockObject; + [[mockObject expect] willSendWithObjectLoader:objectLoader]; + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + [mockObject verify]; +} + +- (void)testShouldInvokeWillSendWithObjectLoaderOnSendSynchronously { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + [objectManager setMappingProvider:[self providerForComplexUser]]; + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + id mockObject = [OCMockObject partialMockForObject:user]; + + // Explicitly init so we don't get a managed object loader... + RKObjectLoader *objectLoader = [RKObjectLoader loaderWithURL:[objectManager.baseURL URLByAppendingResourcePath:@"/200"] mappingProvider:objectManager.mappingProvider]; + objectLoader.sourceObject = mockObject; + [[mockObject expect] willSendWithObjectLoader:objectLoader]; + [objectLoader sendSynchronously]; + [mockObject verify]; +} + +- (void)testShouldLoadResultsNestedAtAKeyPath { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* objectMapping = [RKObjectMapping mappingForClass:[RKObjectLoaderTestResultModel class]]; + [objectMapping mapKeyPath:@"id" toAttribute:@"ID"]; + [objectMapping mapKeyPath:@"ends_at" toAttribute:@"endsAt"]; + [objectMapping mapKeyPath:@"photo_url" toAttribute:@"photoURL"]; + [objectManager.mappingProvider setMapping:objectMapping forKeyPath:@"results"]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [objectManager loadObjectsAtResourcePath:@"/JSON/ArrayOfResults.json" delegate:loader]; + [loader waitForResponse]; + assertThat([loader objects], hasCountOf(2)); + assertThat([[[loader objects] objectAtIndex:0] ID], is(equalToInt(226))); + assertThat([[[loader objects] objectAtIndex:0] photoURL], is(equalTo(@"1308262872.jpg"))); + assertThat([[[loader objects] objectAtIndex:1] ID], is(equalToInt(235))); + assertThat([[[loader objects] objectAtIndex:1] photoURL], is(equalTo(@"1308634984.jpg"))); +} + +- (void)testShouldAllowMutationOfTheParsedDataInWillMapData { + RKTestResponseLoaderWithWillMapData* loader = (RKTestResponseLoaderWithWillMapData*)[RKTestResponseLoaderWithWillMapData responseLoader]; + RKObjectManager* manager = [RKTestFactory objectManager]; + [manager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader]; + [loader waitForResponse]; + assertThat([loader.mappableData valueForKey:@"newKey"], is(equalTo(@"monkey!"))); +} + +- (void)testShouldAllowYouToPostAnObjectAndHandleAnEmpty204Response { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [mapping inverseMapping]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/204"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* loader = [objectManager loaderForObject:user method:RKRequestMethodPOST]; + loader.delegate = responseLoader; + loader.objectMapping = mapping; + [loader send]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThat(user.email, is(equalTo(@"blake@restkit.org"))); +} + +- (void)testShouldAllowYouToPOSTAnObjectAndMapBackNonNestedContent { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [mapping inverseMapping]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/notNestedUser"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* loader = [objectManager loaderForObject:user method:RKRequestMethodPOST]; + loader.delegate = responseLoader; + loader.objectMapping = mapping; + [loader send]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThat(user.email, is(equalTo(@"changed"))); +} + +- (void)testShouldMapContentWithoutAMIMEType { + // TODO: Not sure that this is even worth it. Unable to get the Sinatra server to produce such a response + return; + RKLogConfigureByName("RestKit/Network", RKLogLevelTrace); + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [mapping inverseMapping]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [[RKParserRegistry sharedRegistry] setParserClass:[RKJSONParserJSONKit class] forMIMEType:@"text/html"]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/noMIME"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* loader = [objectManager loaderForObject:user method:RKRequestMethodPOST]; + loader.delegate = responseLoader; + loader.objectMapping = mapping; + [loader send]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThat(user.email, is(equalTo(@"changed"))); +} + +- (void)testShouldAllowYouToPOSTAnObjectOfOneTypeAndGetBackAnother { + RKObjectMapping* sourceMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [sourceMapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [sourceMapping inverseMapping]; + + RKObjectMapping* targetMapping = [RKObjectMapping mappingForClass:[RKObjectLoaderTestResultModel class]]; + [targetMapping mapAttributes:@"ID", nil]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/notNestedUser"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* loader = [objectManager loaderForObject:user method:RKRequestMethodPOST]; + loader.delegate = responseLoader; + loader.sourceObject = user; + loader.targetObject = nil; + loader.objectMapping = targetMapping; + [loader send]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + + // Our original object should not have changed + assertThat(user.email, is(equalTo(@"blake@restkit.org"))); + + // And we should have a new one + RKObjectLoaderTestResultModel* newObject = [[responseLoader objects] lastObject]; + assertThat(newObject, is(instanceOf([RKObjectLoaderTestResultModel class]))); + assertThat(newObject.ID, is(equalToInt(31337))); +} + +- (void)testShouldAllowYouToPOSTAnObjectOfOneTypeAndGetBackAnotherViaURLConfiguration { + RKObjectMapping* sourceMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [sourceMapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [sourceMapping inverseMapping]; + + RKObjectMapping* targetMapping = [RKObjectMapping mappingForClass:[RKObjectLoaderTestResultModel class]]; + [targetMapping mapAttributes:@"ID", nil]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/notNestedUser"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + [objectManager.mappingProvider setObjectMapping:targetMapping forResourcePathPattern:@"/notNestedUser"]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* loader = [objectManager loaderForObject:user method:RKRequestMethodPOST]; + loader.delegate = responseLoader; + loader.sourceObject = user; + loader.targetObject = nil; + [loader send]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + + // Our original object should not have changed + assertThat(user.email, is(equalTo(@"blake@restkit.org"))); + + // And we should have a new one + RKObjectLoaderTestResultModel* newObject = [[responseLoader objects] lastObject]; + assertThat(newObject, is(instanceOf([RKObjectLoaderTestResultModel class]))); + assertThat(newObject.ID, is(equalToInt(31337))); +} + +// TODO: Should live in a different file... +- (void)testShouldAllowYouToPOSTAnObjectAndMapBackNonNestedContentViapostObject { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [mapping mapAttributes:@"firstname", @"lastname", @"email", nil]; + RKObjectMapping* serializationMapping = [mapping inverseMapping]; + + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKTestComplexUser class] toResourcePath:@"/notNestedUser"]; + [objectManager.mappingProvider setSerializationMapping:serializationMapping forClass:[RKTestComplexUser class]]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + // NOTE: The postObject: should infer the target object from sourceObject and the mapping class + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + [objectManager postObject:user usingBlock:^(RKObjectLoader *loader) { + loader.delegate = responseLoader; + loader.objectMapping = mapping; + }]; + [responseLoader waitForResponse]; + assertThatBool([responseLoader wasSuccessful], is(equalToBool(YES))); + assertThat(user.email, is(equalTo(@"changed"))); +} + +- (void)testShouldRespectTheRootKeyPathWhenConstructingATemporaryObjectMappingProvider { + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.objectMapping = userMapping; + objectLoader.method = RKRequestMethodGET; + objectLoader.targetObject = user; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +- (void)testShouldDetermineObjectLoaderBasedOnResourcePathPatternWithExactMatch { + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + [mappingProvider setObjectMapping:userMapping forResourcePathPattern:@"/JSON/ComplexNestedUser.json"]; + + RKURL *URL = [objectManager.baseURL URLByAppendingResourcePath:@"/JSON/ComplexNestedUser.json"]; + RKObjectLoader* objectLoader = [RKObjectLoader loaderWithURL:URL mappingProvider:mappingProvider]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.targetObject = user; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + + NSLog(@"Response: %@", responseLoader.objects); + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +- (void)testShouldDetermineObjectLoaderBasedOnResourcePathPatternWithPartialMatch { + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + [mappingProvider setObjectMapping:userMapping forResourcePathPattern:@"/JSON/:name\\.json"]; + + RKURL *URL = [objectManager.baseURL URLByAppendingResourcePath:@"/JSON/ComplexNestedUser.json"]; + RKObjectLoader* objectLoader = [RKObjectLoader loaderWithURL:URL mappingProvider:mappingProvider]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.targetObject = user; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + + NSLog(@"Response: %@", responseLoader.objects); + + assertThat(user.firstname, is(equalTo(@"Diego"))); +} + +- (void)testShouldReturnSuccessWhenTheStatusCodeIs200AndTheResponseBodyIsEmpty { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/humans/1234"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodDELETE; + objectLoader.objectMapping = userMapping; + objectLoader.targetObject = user; + [objectLoader send]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); +} + +- (void)testShouldInvokeTheDelegateWithTheTargetObjectWhenTheStatusCodeIs200AndTheResponseBodyIsEmpty { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + + RKTestComplexUser* user = [[RKTestComplexUser new] autorelease]; + user.firstname = @"Blake"; + user.lastname = @"Watters"; + user.email = @"blake@restkit.org"; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + + RKObjectLoader* objectLoader = [RKObjectLoader loaderWithURL:[objectManager.baseURL URLByAppendingResourcePath:@"/humans/1234"] mappingProvider:objectManager.mappingProvider]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodDELETE; + objectLoader.objectMapping = userMapping; + objectLoader.targetObject = user; + [objectLoader send]; + [responseLoader waitForResponse]; + assertThat(responseLoader.objects, hasItem(user)); +} + +- (void)testShouldConsiderTheLoadOfEmptyObjectsWithoutAnyMappableAttributesAsSuccess { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + [userMapping mapAttributes:@"firstname", nil]; + [objectManager.mappingProvider setMapping:userMapping forKeyPath:@"firstUser"]; + [objectManager.mappingProvider setMapping:userMapping forKeyPath:@"secondUser"]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + [objectManager loadObjectsAtResourcePath:@"/users/empty" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); +} + +- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyArray { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + responseLoader.timeout = 20; + [objectManager loadObjectsAtResourcePath:@"/empty/array" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThat(responseLoader.objects, isNot(nilValue())); + assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); + assertThat(responseLoader.objects, is(empty())); +} + +- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyDictionary { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + responseLoader.timeout = 20; + [objectManager loadObjectsAtResourcePath:@"/empty/dictionary" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThat(responseLoader.objects, isNot(nilValue())); + assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); + assertThat(responseLoader.objects, is(empty())); +} + +- (void)testShouldInvokeTheDelegateOnSuccessIfTheResponseIsAnEmptyString { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + responseLoader.timeout = 20; + [objectManager loadObjectsAtResourcePath:@"/empty/string" delegate:responseLoader]; + [responseLoader waitForResponse]; + assertThat(responseLoader.objects, isNot(nilValue())); + assertThatBool([responseLoader.objects isKindOfClass:[NSArray class]], is(equalToBool(YES))); + assertThat(responseLoader.objects, is(empty())); +} + +- (void)testShouldNotBlockNetworkOperationsWhileAwaitingObjectMapping { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + objectManager.requestCache.storagePolicy = RKRequestCacheStoragePolicyDisabled; + objectManager.client.requestQueue.concurrentRequestsLimit = 1; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"human"; + [userMapping mapAttributes:@"name", @"id", nil]; + + // Suspend the Queue to block object mapping + dispatch_suspend(objectManager.mappingQueue); + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + [objectManager.mappingProvider setObjectMapping:userMapping forResourcePathPattern:@"/humans/1"]; + [objectManager loadObjectsAtResourcePath:@"/humans/1" delegate:nil]; + [objectManager.client get:@"/empty/string" delegate:responseLoader]; + [responseLoader waitForResponse]; + + // We should get a response is network is released even though object mapping didn't finish + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); +} + +#pragma mark - Block Tests + +- (void)testInvocationOfDidLoadObjectBlock { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObject = ^(id object) { + expectedResult = object; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(expectedResult, is(notNilValue())); +} + +- (void)testInvocationOfDidLoadObjectBlockIsSingularObjectOfCorrectType { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObject = ^(id object) { + expectedResult = object; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(expectedResult, is(instanceOf([RKTestComplexUser class]))); +} + +- (void)testInvocationOfDidLoadObjectsBlock { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObjects = ^(NSArray *objects) { + expectedResult = objects; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(expectedResult, is(notNilValue())); +} + +- (void)testInvocationOfDidLoadObjectsBlocksIsCollectionOfObjects { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObjects = ^(NSArray *objects) { + expectedResult = [objects retain]; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + NSLog(@"The expectedResult = %@", expectedResult); + assertThat(expectedResult, is(instanceOf([NSArray class]))); + assertThat(expectedResult, hasCountOf(1)); + [expectedResult release]; +} + +- (void)testInvocationOfDidLoadObjectsDictionaryBlock { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObjectsDictionary = ^(NSDictionary *dictionary) { + expectedResult = dictionary; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(expectedResult, is(notNilValue())); +} + +- (void)testInvocationOfDidLoadObjectsDictionaryBlocksIsDictionaryOfObjects { + RKObjectManager* objectManager = [RKObjectManager managerWithBaseURL:[RKTestFactory baseURL]]; + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/JSON/ComplexNestedUser.json"]; + objectLoader.delegate = responseLoader; + objectLoader.method = RKRequestMethodGET; + objectLoader.mappingProvider = [self providerForComplexUser]; + __block id expectedResult = nil; + objectLoader.onDidLoadObjectsDictionary = ^(NSDictionary *dictionary) { + expectedResult = dictionary; + }; + + [objectLoader sendAsynchronously]; + [responseLoader waitForResponse]; + assertThat(expectedResult, is(instanceOf([NSDictionary class]))); + assertThat(expectedResult, hasCountOf(1)); +} + +// NOTE: Errors are fired in a number of contexts within the RKObjectLoader. We have centralized the cases into a private +// method and test that one case here. There should be better coverage for this. +- (void)testInvocationOfOnDidFailWithError { + RKObjectLoader *loader = [RKObjectLoader loaderWithURL:nil mappingProvider:nil]; + NSError *expectedError = [NSError errorWithDomain:@"Testing" code:1234 userInfo:nil]; + __block NSError *blockError = nil; + loader.onDidFailWithError = ^(NSError *error) { + blockError = error; + }; + [loader performSelector:@selector(informDelegateOfError:) withObject:expectedError]; + assertThat(blockError, is(equalTo(expectedError))); +} + +- (void)testShouldNotAssertDuringObjectMappingOnSynchronousRequest { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestComplexUser class]]; + userMapping.rootKeyPath = @"data.STUser"; + [userMapping mapAttributes:@"firstname", nil]; + RKObjectLoader* objectLoader = [objectManager loaderWithResourcePath:@"/humans/1"]; + objectLoader.objectMapping = userMapping; + [objectLoader sendSynchronously]; + RKResponse *response = [objectLoader sendSynchronously]; + + assertThatInteger(response.statusCode, is(equalToInt(200))); +} + +@end diff --git a/Specs/ObjectMapping/RKObjectManagerSpec.m b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m similarity index 52% rename from Specs/ObjectMapping/RKObjectManagerSpec.m rename to Tests/Logic/ObjectMapping/RKObjectManagerTest.m index 3e0e82f4f1..b73435fd34 100644 --- a/Specs/ObjectMapping/RKObjectManagerSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectManagerTest.m @@ -1,16 +1,16 @@ // -// RKObjectManagerSpec.m +// RKObjectManagerTest.m // RestKit // // Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,33 +18,35 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKObjectManager.h" #import "RKManagedObjectStore.h" -#import "RKSpecResponseLoader.h" +#import "RKTestResponseLoader.h" #import "RKManagedObjectMapping.h" #import "RKObjectMappingProvider.h" #import "RKHuman.h" #import "RKCat.h" -#import "RKObjectMapperSpecModel.h" +#import "RKObjectMapperTestModel.h" -@interface RKObjectManagerSpec : RKSpec { - RKObjectManager* _objectManager; +@interface RKObjectManagerTest : RKTestCase { + RKObjectManager* _objectManager; } @end -@implementation RKObjectManagerSpec +@implementation RKObjectManagerTest - (void)setUp { - _objectManager = RKSpecNewObjectManager(); - _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKSpecs.sqlite"]; + [RKTestFactory setUp]; + + _objectManager = [RKTestFactory objectManager]; + _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; [RKObjectManager setSharedManager:_objectManager]; - [_objectManager.objectStore deletePersistantStore]; - + [_objectManager.objectStore deletePersistentStore]; + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - - RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class]]; + + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:_objectManager.objectStore]; humanMapping.rootKeyPath = @"human"; [humanMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]]; [humanMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"nick-name" toKeyPath:@"nickName"]]; @@ -54,8 +56,8 @@ - (void)setUp { [humanMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"created-at" toKeyPath:@"createdAt"]]; [humanMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"updated-at" toKeyPath:@"updatedAt"]]; [humanMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - - RKManagedObjectMapping* catObjectMapping = [RKManagedObjectMapping mappingForClass:[RKCat class]]; + + RKManagedObjectMapping* catObjectMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:_objectManager.objectStore]; [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]]; [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"nick-name" toKeyPath:@"nickName"]]; [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"birthday" toKeyPath:@"birthday"]]; @@ -64,42 +66,45 @@ - (void)setUp { [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"created-at" toKeyPath:@"createdAt"]]; [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"updated-at" toKeyPath:@"updatedAt"]]; [catObjectMapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - + [catObjectMapping addRelationshipMapping:[RKObjectRelationshipMapping mappingFromKeyPath:@"cats" toKeyPath:@"cats" withMapping:catObjectMapping]]; - + [provider setMapping:humanMapping forKeyPath:@"human"]; [provider setMapping:humanMapping forKeyPath:@"humans"]; - + RKObjectMapping* humanSerialization = [RKObjectMapping mappingForClass:[NSDictionary class]]; [humanSerialization addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]]; [provider setSerializationMapping:humanSerialization forClass:[RKHuman class]]; _objectManager.mappingProvider = provider; - + RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; [router routeClass:[RKHuman class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST]; _objectManager.router = router; - -// RKSpecStubNetworkAvailability(YES); +} + +- (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... - (void)testShouldUpdateACoreDataBackedTargetObject { - RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.managedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.managedObjectContext]; + RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext]; temporaryHuman.name = @"My Name"; - + // TODO: We should NOT have to save the object store here to make this // spec pass. Without it we are crashing inside the mapper internals. Believe // that we just need a way to save the context before we begin mapping or something // on success. Always saving means that we can abandon objects on failure... - [_objectManager.objectStore save]; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + [_objectManager.objectStore save:nil]; + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; [_objectManager postObject:temporaryHuman delegate:loader]; [loader waitForResponse]; - + assertThat(loader.objects, isNot(empty())); RKHuman* human = (RKHuman*)[loader.objects objectAtIndex:0]; assertThat(human, is(equalTo(temporaryHuman))); @@ -107,168 +112,178 @@ - (void)testShouldUpdateACoreDataBackedTargetObject { } - (void)testShouldDeleteACoreDataBackedTargetObjectOnError { - RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.managedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.managedObjectContext]; + RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext]; temporaryHuman.name = @"My Name"; RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; [mapping mapAttributes:@"name", nil]; - - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; NSString* resourcePath = @"/humans/fail"; - RKObjectLoader* objectLoader = [_objectManager objectLoaderWithResourcePath:resourcePath delegate:loader]; + RKObjectLoader* objectLoader = [_objectManager loaderWithResourcePath:resourcePath]; + objectLoader.delegate = loader; objectLoader.method = RKRequestMethodPOST; objectLoader.targetObject = temporaryHuman; objectLoader.serializationMapping = mapping; - [objectLoader send]; + [objectLoader send]; [loader waitForResponse]; assertThat(temporaryHuman.managedObjectContext, is(equalTo(nil))); } - (void)testShouldNotDeleteACoreDataBackedTargetObjectOnErrorIfItWasAlreadySaved { - RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.managedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.managedObjectContext]; + RKHuman* temporaryHuman = [[RKHuman alloc] initWithEntity:[NSEntityDescription entityForName:@"RKHuman" inManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext] insertIntoManagedObjectContext:_objectManager.objectStore.primaryManagedObjectContext]; temporaryHuman.name = @"My Name"; RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; [mapping mapAttributes:@"name", nil]; - + // Save it to suppress deletion - [_objectManager.objectStore save]; - - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + [_objectManager.objectStore save:nil]; + + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; NSString* resourcePath = @"/humans/fail"; - RKObjectLoader* objectLoader = [_objectManager objectLoaderWithResourcePath:resourcePath delegate:loader]; + RKObjectLoader* objectLoader = [_objectManager loaderWithResourcePath:resourcePath]; + objectLoader.delegate = loader; objectLoader.method = RKRequestMethodPOST; objectLoader.targetObject = temporaryHuman; objectLoader.serializationMapping = mapping; - [objectLoader send]; + [objectLoader send]; [loader waitForResponse]; - - assertThat(temporaryHuman.managedObjectContext, is(equalTo(_objectManager.objectStore.managedObjectContext))); + + assertThat(temporaryHuman.managedObjectContext, is(equalTo(_objectManager.objectStore.primaryManagedObjectContext))); } // TODO: Move to Core Data specific spec file... - (void)testShouldLoadAHuman { assertThatBool([RKClient sharedClient].isNetworkReachable, is(equalToBool(YES))); - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - [_objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:loader]; - [loader waitForResponse]; - assertThat(loader.failureError, is(nilValue())); + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [_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 { - RKSpecResponseLoader* loader = [RKSpecResponseLoader 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]))); + 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]))); } - (void)testShouldHandleConnectionFailures { - NSString* localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; - RKObjectManager* modelManager = [RKObjectManager objectManagerWithBaseURL:localBaseURL]; + NSString* localBaseURL = [NSString stringWithFormat:@"http://127.0.0.1:3001"]; + RKObjectManager* modelManager = [RKObjectManager managerWithBaseURLString:localBaseURL]; modelManager.client.requestQueue.suspended = NO; - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; - [modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader]; - [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(NO))); + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; + [modelManager loadObjectsAtResourcePath:@"/JSON/humans/1" delegate:loader]; + [loader waitForResponse]; + assertThatBool(loader.wasSuccessful, is(equalToBool(NO))); } - (void)testShouldPOSTAnObject { - RKObjectManager* manager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - assertThatBool([RKClient sharedClient].isNetworkReachable, is(equalToBool(YES))); - + RKObjectManager* manager = [RKTestFactory objectManager]; + RKObjectRouter* router = [[RKObjectRouter new] autorelease]; - [router routeClass:[RKObjectMapperSpecModel class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST]; + [router routeClass:[RKObjectMapperTestModel class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST]; manager.router = router; - - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; mapping.rootKeyPath = @"human"; [mapping mapAttributes:@"name", @"age", nil]; [manager.mappingProvider setMapping:mapping forKeyPath:@"human"]; - [manager.mappingProvider setSerializationMapping:mapping forClass:[RKObjectMapperSpecModel class]]; - - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + [manager.mappingProvider setSerializationMapping:mapping forClass:[RKObjectMapperTestModel class]]; + + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; [manager postObject:human delegate:loader]; [loader waitForResponse]; - + // NOTE: The /humans endpoint returns a canned response, we are testing the plumbing // of the object manager here. assertThat(human.name, is(equalTo(@"My Name"))); } - (void)testShouldNotSetAContentBodyOnAGET { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKObjectMapperSpecModel class] toResourcePath:@"/humans/1"]; - - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKObjectMapperTestModel class] toResourcePath:@"/humans/1"]; + + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; [mapping mapAttributes:@"name", @"age", nil]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - RKObjectLoader* loader = [objectManager getObject:human delegate:responseLoader]; + __block RKObjectLoader *objectLoader = nil; + [objectManager getObject:human usingBlock:^(RKObjectLoader *loader) { + loader.delegate = responseLoader; + objectLoader = loader; + }]; [responseLoader waitForResponse]; - RKLogCritical(@"%@", [loader.URLRequest allHTTPHeaderFields]); - assertThat([loader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); + RKLogCritical(@"%@", [objectLoader.URLRequest allHTTPHeaderFields]); + assertThat([objectLoader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); } - (void)testShouldNotSetAContentBodyOnADELETE { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKObjectMapperSpecModel class] toResourcePath:@"/humans/1"]; - - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKObjectMapperTestModel class] toResourcePath:@"/humans/1"]; + + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; [mapping mapAttributes:@"name", @"age", nil]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - RKObjectLoader* loader = [objectManager deleteObject:human delegate:responseLoader]; + __block RKObjectLoader *objectLoader = nil; + [objectManager deleteObject:human usingBlock:^(RKObjectLoader *loader) { + loader.delegate = responseLoader; + objectLoader = loader; + }]; [responseLoader waitForResponse]; - RKLogCritical(@"%@", [loader.URLRequest allHTTPHeaderFields]); - assertThat([loader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); + RKLogCritical(@"%@", [objectLoader.URLRequest allHTTPHeaderFields]); + assertThat([objectLoader.URLRequest valueForHTTPHeaderField:@"Content-Length"], is(equalTo(@"0"))); } #pragma mark - Block Helpers - (void)testShouldLetYouLoadObjectsWithABlock { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; [mapping mapAttributes:@"name", @"age", nil]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - [objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" delegate:responseLoader block:^(RKObjectLoader* loader) { + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + [objectManager loadObjectsAtResourcePath:@"/JSON/humans/1.json" usingBlock:^(RKObjectLoader* loader) { + loader.delegate = responseLoader; loader.objectMapping = mapping; }]; [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(YES))); + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); assertThat(responseLoader.objects, hasCountOf(1)); } - (void)testShouldAllowYouToOverrideTheRoutedResourcePath { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - [objectManager.router routeClass:[RKObjectMapperSpecModel class] toResourcePath:@"/humans/2"]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + [objectManager.router routeClass:[RKObjectMapperTestModel class] toResourcePath:@"/humans/2"]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; [mapping mapAttributes:@"name", @"age", nil]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - [objectManager deleteObject:human delegate:responseLoader block:^(RKObjectLoader* loader) { + [objectManager deleteObject:human usingBlock:^(RKObjectLoader* loader) { + loader.delegate = responseLoader; loader.resourcePath = @"/humans/1"; }]; [responseLoader waitForResponse]; @@ -276,16 +291,18 @@ - (void)testShouldAllowYouToOverrideTheRoutedResourcePath { } - (void)testShouldAllowYouToUseObjectHelpersWithoutRouting { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; [mapping mapAttributes:@"name", @"age", nil]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - [objectManager deleteObject:human delegate:responseLoader block:^(RKObjectLoader* loader) { + [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { + loader.method = RKRequestMethodDELETE; + loader.delegate = responseLoader; loader.resourcePath = @"/humans/1"; }]; [responseLoader waitForResponse]; @@ -293,43 +310,102 @@ - (void)testShouldAllowYouToUseObjectHelpersWithoutRouting { } - (void)testShouldAllowYouToSkipTheMappingProvider { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; mapping.rootKeyPath = @"human"; [mapping mapAttributes:@"name", @"age", nil]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; - [objectManager deleteObject:human delegate:responseLoader block:^(RKObjectLoader* loader) { - loader.resourcePath = @"/humans/1"; + [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { + loader.method = RKRequestMethodDELETE; + loader.delegate = responseLoader; loader.objectMapping = mapping; }]; [responseLoader waitForResponse]; - assertThatBool(responseLoader.success, is(equalToBool(YES))); - assertThat(responseLoader.response.request.resourcePath, is(equalTo(@"/humans/1"))); + assertThatBool(responseLoader.wasSuccessful, is(equalToBool(YES))); + assertThat(responseLoader.response.request.resourcePath, is(equalTo(@"/humans/1"))); } - (void)testShouldLetYouOverloadTheParamsOnAnObjectLoaderRequest { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperSpecModel class]]; + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; mapping.rootKeyPath = @"human"; [mapping mapAttributes:@"name", @"age", nil]; - - RKSpecResponseLoader* responseLoader = [RKSpecResponseLoader responseLoader]; - RKObjectMapperSpecModel* human = [[RKObjectMapperSpecModel new] autorelease]; + + RKTestResponseLoader* responseLoader = [RKTestResponseLoader responseLoader]; + RKObjectMapperTestModel* human = [[RKObjectMapperTestModel new] autorelease]; human.name = @"Blake Watters"; human.age = [NSNumber numberWithInt:28]; NSDictionary *myParams = [NSDictionary dictionaryWithObject:@"bar" forKey:@"foo"]; - RKObjectLoader* loader = [objectManager sendObject:human delegate:responseLoader block:^(RKObjectLoader* loader) { + __block RKObjectLoader* objectLoader = nil; + [objectManager sendObject:human toResourcePath:@"/humans/1" usingBlock:^(RKObjectLoader *loader) { + loader.delegate = responseLoader; loader.method = RKRequestMethodPOST; - loader.resourcePath = @"/humans/1"; loader.objectMapping = mapping; loader.params = myParams; + objectLoader = loader; }]; [responseLoader waitForResponse]; - assertThat(loader.params, is(equalTo(myParams))); + assertThat(objectLoader.params, is(equalTo(myParams))); +} + +- (void)testInitializationOfObjectLoaderViaManagerConfiguresSerializationMIMEType { + RKObjectManager *objectManager = [RKTestFactory objectManager]; + objectManager.serializationMIMEType = RKMIMETypeJSON; + RKObjectLoader *loader = [objectManager loaderWithResourcePath:@"/test"]; + assertThat(loader.serializationMIMEType, isNot(nilValue())); + assertThat(loader.serializationMIMEType, is(equalTo(RKMIMETypeJSON))); +} + +- (void)testInitializationOfRoutedPathViaSendObjectMethodUsingBlock +{ + RKObjectManager *objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; + mapping.rootKeyPath = @"human"; + [objectManager.mappingProvider registerObjectMapping:mapping withRootKeyPath:@"human"]; + [objectManager.router 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; + }]; + [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/Specs/ObjectMapping/RKObjectMappingNextGenSpec.m b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m similarity index 76% rename from Specs/ObjectMapping/RKObjectMappingNextGenSpec.m rename to Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m index d2e964ba8f..24fc769fbf 100644 --- a/Specs/ObjectMapping/RKObjectMappingNextGenSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m @@ -1,16 +1,16 @@ // -// RKObjectMappingNextGenSpec.m +// RKObjectMappingNextGenTest.m // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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 "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKObjectMapping.h" #import "RKObjectMappingOperation.h" #import "RKObjectAttributeMapping.h" @@ -30,128 +30,14 @@ #import "RKObjectMapper_Private.h" #import "RKObjectMapperError.h" #import "RKDynamicMappingModels.h" +#import "RKTestAddress.h" +#import "RKTestUser.h" +#import "RKObjectMappingProvider+Contexts.h" -// Managed Object Serialization Specific +// Managed Object Serialization Testific #import "RKHuman.h" #import "RKCat.h" -//////////////////////////////////////////////////////////////////////////////// - -@interface RKSpecAddress : NSObject { - NSNumber* _addressID; - NSString* _city; - NSString* _state; - NSString* _country; -} - -@property (nonatomic, retain) NSNumber* addressID; -@property (nonatomic, retain) NSString* city; -@property (nonatomic, retain) NSString* state; -@property (nonatomic, retain) NSString* country; - -@end - -@implementation RKSpecAddress - -@synthesize addressID = _addressID; -@synthesize city = _city; -@synthesize state = _state; -@synthesize country = _country; - -+ (RKSpecAddress*)address { - return [[self new] autorelease]; -} - -// isEqual: is consulted by the mapping operation -// to determine if assocation values should be set -- (BOOL)isEqual:(id)object { - if ([object isKindOfClass:[RKSpecAddress class]]) { - return [[(RKSpecAddress*)object addressID] isEqualToNumber:self.addressID]; - } else { - return NO; - } -} - -@end - -@interface RKExampleUser : NSObject { - NSNumber* _userID; - NSString* _name; - NSDate* _birthDate; - NSArray* _favoriteColors; - NSDictionary* _addressDictionary; - NSURL* _website; - NSNumber* _isDeveloper; - NSNumber* _luckyNumber; - NSDecimalNumber* _weight; - NSArray* _interests; - NSString* _country; - - // Relationships - RKSpecAddress* _address; - NSArray* _friends; -} - -@property (nonatomic, retain) NSNumber* userID; -@property (nonatomic, retain) NSString* name; -@property (nonatomic, retain) NSDate* birthDate; -@property (nonatomic, retain) NSArray* favoriteColors; -@property (nonatomic, retain) NSDictionary* addressDictionary; -@property (nonatomic, retain) NSURL* website; -@property (nonatomic, retain) NSNumber* isDeveloper; -@property (nonatomic, retain) NSNumber* luckyNumber; -@property (nonatomic, retain) NSDecimalNumber* weight; -@property (nonatomic, retain) NSArray* interests; -@property (nonatomic, retain) NSString* country; -@property (nonatomic, retain) RKSpecAddress* address; -@property (nonatomic, retain) NSArray* friends; -@property (nonatomic, retain) NSSet* friendsSet; - -@end - -@implementation RKExampleUser - -@synthesize userID = _userID; -@synthesize name = _name; -@synthesize birthDate = _birthDate; -@synthesize favoriteColors = _favoriteColors; -@synthesize addressDictionary = _addressDictionary; -@synthesize website = _website; -@synthesize isDeveloper = _isDeveloper; -@synthesize luckyNumber = _luckyNumber; -@synthesize weight = _weight; -@synthesize interests = _interests; -@synthesize country = _country; -@synthesize address = _address; -@synthesize friends = _friends; -@synthesize friendsSet = _friendsSet; - -+ (RKExampleUser*)user { - return [[self new] autorelease]; -} - -// isEqual: is consulted by the mapping operation -// to determine if assocation values should be set -- (BOOL)isEqual:(id)object { - if ([object isKindOfClass:[RKExampleUser class]]) { - return [[(RKExampleUser*)object userID] isEqualToNumber:self.userID]; - } else { - return NO; - } -} - -- (id)valueForUndefinedKey:(NSString *)key { - RKLogError(@"Unexpectedly asked for undefined key '%@'", key); - return [super valueForUndefinedKey:key]; -} - -- (void)setValue:(id)value forUndefinedKey:(NSString *)key { - RKLogError(@"Asked to set value '%@' for undefined key '%@'", value, key); - [super setValue:value forUndefinedKey:key]; -} - -@end - @interface RKExampleGroupWithUserArray : NSObject { NSString * _name; NSArray* _users; @@ -218,15 +104,15 @@ - (BOOL)isEqual:(id)object { #pragma mark - -@interface RKObjectMappingNextGenSpec : RKSpec { - +@interface RKObjectMappingNextGenTest : RKTestCase { + } @end -@implementation RKObjectMappingNextGenSpec +@implementation RKObjectMappingNextGenTest -#pragma mark - RKObjectKeyPathMapping Specs +#pragma mark - RKObjectKeyPathMapping Tests - (void)testShouldDefineElementToPropertyMapping { RKObjectAttributeMapping* elementMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; @@ -239,17 +125,17 @@ - (void)testShouldDescribeElementMappings { assertThat([elementMapping description], is(equalTo(@"RKObjectKeyPathMapping: id => userID"))); } -#pragma mark - RKObjectMapping Specs +#pragma mark - RKObjectMapping Tests - (void)testShouldDefineMappingFromAnElementToAProperty { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; assertThat([mapping mappingForKeyPath:@"id"], is(sameInstance(idMapping))); } - (void)testShouldAddMappingsToAttributeMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; assertThatBool([mapping.mappings containsObject:idMapping], is(equalToBool(YES))); @@ -257,7 +143,7 @@ - (void)testShouldAddMappingsToAttributeMappings { } - (void)testShouldAddMappingsToRelationshipMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectRelationshipMapping* idMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"id" toKeyPath:@"userID" withMapping:nil]; [mapping addRelationshipMapping:idMapping]; assertThatBool([mapping.mappings containsObject:idMapping], is(equalToBool(YES))); @@ -265,14 +151,14 @@ - (void)testShouldAddMappingsToRelationshipMappings { } - (void)testShouldGenerateAttributeMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; assertThat([mapping mappingForKeyPath:@"name"], is(nilValue())); [mapping mapKeyPath:@"name" toAttribute:@"name"]; assertThat([mapping mappingForKeyPath:@"name"], isNot(nilValue())); } - (void)testShouldGenerateRelationshipMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMapping* anotherMapping = [RKObjectMapping mappingForClass:[NSDictionary class]]; assertThat([mapping mappingForKeyPath:@"another"], is(nilValue())); [mapping mapRelationship:@"another" withMapping:anotherMapping]; @@ -280,7 +166,7 @@ - (void)testShouldGenerateRelationshipMappings { } - (void)testShouldRemoveMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; assertThat(mapping.mappings, hasItem(idMapping)); @@ -289,7 +175,7 @@ - (void)testShouldRemoveMappings { } - (void)testShouldRemoveMappingsByKeyPath { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; assertThat(mapping.mappings, hasItem(idMapping)); @@ -298,7 +184,7 @@ - (void)testShouldRemoveMappingsByKeyPath { } - (void)testShouldRemoveAllMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [mapping mapAttributes:@"one", @"two", @"three", nil]; assertThat(mapping.mappings, hasCountOf(3)); [mapping removeAllMappings]; @@ -306,10 +192,10 @@ - (void)testShouldRemoveAllMappings { } - (void)testShouldGenerateAnInverseMappings { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [mapping mapKeyPath:@"first_name" toAttribute:@"firstName"]; [mapping mapAttributes:@"city", @"state", @"zip", nil]; - RKObjectMapping* otherMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* otherMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [otherMapping mapAttributes:@"street", nil]; [mapping mapRelationship:@"address" withMapping:otherMapping]; RKObjectMapping* inverse = [mapping inverseMapping]; @@ -318,31 +204,31 @@ - (void)testShouldGenerateAnInverseMappings { } - (void)testShouldLetYouRetrieveMappingsByAttribute { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* attributeMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"nameAttribute"]; [mapping addAttributeMapping:attributeMapping]; assertThat([mapping mappingForAttribute:@"nameAttribute"], is(equalTo(attributeMapping))); } - (void)testShouldLetYouRetrieveMappingsByRelationship { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectRelationshipMapping* relationshipMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"friend" toKeyPath:@"friendRelationship" withMapping:mapping]; [mapping addRelationshipMapping:relationshipMapping]; assertThat([mapping mappingForRelationship:@"friendRelationship"], is(equalTo(relationshipMapping))); } -#pragma mark - RKObjectMapper Specs +#pragma mark - RKObjectMapper Tests - (void)testShouldPerformBasicMapping { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:mapping]; [mapper release]; assertThatBool(success, is(equalToBool(YES))); @@ -351,42 +237,42 @@ - (void)testShouldPerformBasicMapping { } - (void)testShouldMapACollectionOfSimpleObjectDictionaries { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"users.json"); + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; NSArray* users = [mapper mapCollection:userInfo atKeyPath:@"" usingMapping:mapping]; assertThatUnsignedInteger([users count], is(equalToInt(3))); - RKExampleUser* blake = [users objectAtIndex:0]; + RKTestUser* blake = [users objectAtIndex:0]; assertThat(blake.name, is(equalTo(@"Blake Watters"))); [mapper release]; } - + - (void)testShouldDetermineTheObjectMappingByConsultingTheMappingProviderWhenThereIsATargetObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; - mapper.targetObject = [RKExampleUser user]; + mapper.targetObject = [RKTestUser user]; [mapper performMapping]; - + [mockProvider verify]; } - (void)testShouldAddAnErrorWhenTheKeyPathMappingAndObjectClassDoNotAgree { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; mapper.targetObject = [NSDictionary new]; [mapper performMapping]; @@ -394,22 +280,22 @@ - (void)testShouldAddAnErrorWhenTheKeyPathMappingAndObjectClassDoNotAgree { } - (void)testShouldMapToATargetObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; mapper.targetObject = user; RKObjectMappingResult* result = [mapper performMapping]; - + [mockProvider verify]; assertThat(result, isNot(nilValue())); assertThatBool([result asObject] == user, is(equalToBool(YES))); @@ -417,116 +303,116 @@ - (void)testShouldMapToATargetObject { } - (void)testShouldCreateANewInstanceOfTheAppropriateDestinationObjectWhenThereIsNoTargetObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; id mappingResult = [[mapper performMapping] asObject]; - assertThatBool([mappingResult isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([mappingResult isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); } - (void)testShouldDetermineTheMappingClassForAKeyPathByConsultingTheMappingProviderWhenMappingADictionaryWithoutATargetObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - [[mockProvider expect] mappingsByKeyPath]; - - id userInfo = RKSpecParseFixture(@"user.json"); + [[mockProvider expect] valueForContext:RKObjectMappingProviderContextObjectsByKeyPath]; + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; [mapper performMapping]; [mockProvider verify]; } - (void)testShouldMapWithoutATargetMapping { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; - RKExampleUser* user = [[mapper performMapping] asObject]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [[mapper performMapping] asObject]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); } - (void)testShouldMapACollectionOfObjects { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"users.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; NSArray* users = [result asCollection]; assertThatBool([users isKindOfClass:[NSArray class]], is(equalToBool(YES))); assertThatUnsignedInteger([users count], is(equalToInt(3))); - RKExampleUser* user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [users objectAtIndex:0]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); } - (void)testShouldMapACollectionOfObjectsWithDynamicKeys { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; mapping.forceCollectionMapping = YES; - [mapping mapKeyOfNestedDictionaryToAttribute:@"name"]; + [mapping mapKeyOfNestedDictionaryToAttribute:@"name"]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"(name).id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@"users"]; - - id userInfo = RKSpecParseFixture(@"DynamicKeys.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeys.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; NSArray* users = [result asCollection]; assertThatBool([users isKindOfClass:[NSArray class]], is(equalToBool(YES))); assertThatUnsignedInteger([users count], is(equalToInt(2))); - RKExampleUser* user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [users objectAtIndex:0]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"blake"))); user = [users objectAtIndex:1]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"rachit"))); } - (void)testShouldMapACollectionOfObjectsWithDynamicKeysAndRelationships { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; mapping.forceCollectionMapping = YES; [mapping mapKeyOfNestedDictionaryToAttribute:@"name"]; - - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [addressMapping mapAttributes:@"city", @"state", nil]; [mapping mapKeyPath:@"(name).address" toRelationship:@"address" withMapping:addressMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@"users"]; - - id userInfo = RKSpecParseFixture(@"DynamicKeysWithRelationship.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithRelationship.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; NSArray* users = [result asCollection]; assertThatBool([users isKindOfClass:[NSArray class]], is(equalToBool(YES))); assertThatUnsignedInteger([users count], is(equalToInt(2))); - RKExampleUser* user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [users objectAtIndex:0]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"blake"))); user = [users objectAtIndex:1]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"rachit"))); assertThat(user.address, isNot(nilValue())); assertThat(user.address.city, is(equalTo(@"New York"))); @@ -536,13 +422,13 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndArrayRelationships { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleGroupWithUserArray class]]; [mapping mapAttributes:@"name", nil]; - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; userMapping.forceCollectionMapping = YES; [userMapping mapKeyOfNestedDictionaryToAttribute:@"name"]; [mapping mapKeyPath:@"users" toRelationship:@"users" withMapping:userMapping]; - - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [addressMapping mapAttributes: @"city", @"city", @"state", @"state", @@ -552,36 +438,35 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndArrayRelationships { [userMapping mapKeyPath:@"(name).address" toRelationship:@"address" withMapping:addressMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@"groups"]; - - id userInfo = RKSpecParseFixture(@"DynamicKeysWithNestedRelationship.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithNestedRelationship.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; - + NSArray* groups = [result asCollection]; assertThatBool([groups isKindOfClass:[NSArray class]], is(equalToBool(YES))); assertThatUnsignedInteger([groups count], is(equalToInt(2))); - + RKExampleGroupWithUserArray* group = [groups objectAtIndex:0]; assertThatBool([group isKindOfClass:[RKExampleGroupWithUserArray class]], is(equalToBool(YES))); assertThat(group.name, is(equalTo(@"restkit"))); NSArray * users = group.users; - assertThatUnsignedInteger([users count], is(equalToInt(2))); - RKExampleUser* user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [users objectAtIndex:0]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"blake"))); user = [users objectAtIndex:1]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"rachit"))); assertThat(user.address, isNot(nilValue())); assertThat(user.address.city, is(equalTo(@"New York"))); - + group = [groups objectAtIndex:1]; assertThatBool([group isKindOfClass:[RKExampleGroupWithUserArray class]], is(equalToBool(YES))); assertThat(group.name, is(equalTo(@"others"))); users = group.users; assertThatUnsignedInteger([users count], is(equalToInt(1))); user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"bjorn"))); assertThat(user.address, isNot(nilValue())); assertThat(user.address.city, is(equalTo(@"Gothenburg"))); @@ -591,14 +476,14 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndArrayRelationships { - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndSetRelationships { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleGroupWithUserSet class]]; [mapping mapAttributes:@"name", nil]; - - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; userMapping.forceCollectionMapping = YES; [userMapping mapKeyOfNestedDictionaryToAttribute:@"name"]; [mapping mapKeyPath:@"users" toRelationship:@"users" withMapping:userMapping]; - - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [addressMapping mapAttributes: @"city", @"city", @"state", @"state", @@ -608,40 +493,39 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndSetRelationships { [userMapping mapKeyPath:@"(name).address" toRelationship:@"address" withMapping:addressMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@"groups"]; - - id userInfo = RKSpecParseFixture(@"DynamicKeysWithNestedRelationship.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"DynamicKeysWithNestedRelationship.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; - + NSArray* groups = [result asCollection]; assertThatBool([groups isKindOfClass:[NSArray class]], is(equalToBool(YES))); assertThatUnsignedInteger([groups count], is(equalToInt(2))); - + RKExampleGroupWithUserSet* group = [groups objectAtIndex:0]; assertThatBool([group isKindOfClass:[RKExampleGroupWithUserSet class]], is(equalToBool(YES))); assertThat(group.name, is(equalTo(@"restkit"))); - - + + NSSortDescriptor * sortByName =[[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES] autorelease]; NSArray * descriptors = [NSArray arrayWithObject:sortByName];; NSArray * users = [group.users sortedArrayUsingDescriptors:descriptors]; - assertThatUnsignedInteger([users count], is(equalToInt(2))); - RKExampleUser* user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + RKTestUser* user = [users objectAtIndex:0]; + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"blake"))); user = [users objectAtIndex:1]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"rachit"))); assertThat(user.address, isNot(nilValue())); assertThat(user.address.city, is(equalTo(@"New York"))); - + group = [groups objectAtIndex:1]; assertThatBool([group isKindOfClass:[RKExampleGroupWithUserSet class]], is(equalToBool(YES))); assertThat(group.name, is(equalTo(@"others"))); users = [group.users sortedArrayUsingDescriptors:descriptors]; assertThatUnsignedInteger([users count], is(equalToInt(1))); user = [users objectAtIndex:0]; - assertThatBool([user isKindOfClass:[RKExampleUser class]], is(equalToBool(YES))); + assertThatBool([user isKindOfClass:[RKTestUser class]], is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"bjorn"))); assertThat(user.address, isNot(nilValue())); assertThat(user.address.city, is(equalTo(@"Gothenburg"))); @@ -649,7 +533,7 @@ - (void)testShouldMapANestedArrayOfObjectsWithDynamicKeysAndSetRelationships { } -- (void)testShouldBeAbleToMapFromAUserObjectToADictionary { +- (void)testShouldBeAbleToMapFromAUserObjectToADictionary { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"userID" toKeyPath:@"id"]; [mapping addAttributeMapping:idMapping]; @@ -657,11 +541,11 @@ - (void)testShouldBeAbleToMapFromAUserObjectToADictionary { [mapping addAttributeMapping:nameMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - RKExampleUser* user = [RKExampleUser user]; + + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; user.userID = [NSNumber numberWithInt:123]; - + RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:user mappingProvider:provider]; RKObjectMappingResult* result = [mapper performMapping]; NSDictionary* userInfo = [result asObject]; @@ -670,19 +554,19 @@ - (void)testShouldBeAbleToMapFromAUserObjectToADictionary { } - (void)testShouldMapRegisteredSubKeyPathsOfAnUnmappableDictionaryAndReturnTheResults { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@"user"]; - - id userInfo = RKSpecParseFixture(@"nested_user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"nested_user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; NSDictionary* dictionary = [[mapper performMapping] asDictionary]; assertThatBool([dictionary isKindOfClass:[NSDictionary class]], is(equalToBool(YES))); - RKExampleUser* user = [dictionary objectForKey:@"user"]; + RKTestUser* user = [dictionary objectForKey:@"user"]; assertThat(user, isNot(nilValue())); assertThat(user.name, is(equalTo(@"Blake Watters"))); } @@ -690,24 +574,24 @@ - (void)testShouldMapRegisteredSubKeyPathsOfAnUnmappableDictionaryAndReturnTheRe #pragma mark Mapping Error States - (void)testShouldAddAnErrorWhenYouTryToMapAnArrayToATargetObject { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"users.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; - mapper.targetObject = [RKExampleUser user]; + mapper.targetObject = [RKTestUser user]; [mapper performMapping]; assertThatUnsignedInteger([mapper errorCount], is(equalToInt(1))); assertThatInteger([[mapper.errors objectAtIndex:0] code], is(equalToInt(RKObjectMapperErrorObjectMappingTypeMismatch))); } - (void)testShouldAddAnErrorWhenAttemptingToMapADictionaryWithoutAnObjectMapping { - id userInfo = RKSpecParseFixture(@"user.json"); + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [mapper performMapping]; @@ -717,22 +601,22 @@ - (void)testShouldAddAnErrorWhenAttemptingToMapADictionaryWithoutAnObjectMapping - (void)testShouldAddAnErrorWhenAttemptingToMapACollectionWithoutAnObjectMapping { RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - id userInfo = RKSpecParseFixture(@"users.json"); + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [mapper performMapping]; assertThatUnsignedInteger([mapper errorCount], is(equalToInt(1))); assertThat([[mapper.errors objectAtIndex:0] localizedDescription], is(equalTo(@"Could not find an object mapping for keyPath: ''"))); } -#pragma mark RKObjectMapperDelegate Specs +#pragma mark RKObjectMapperDelegate Tests - (void)testShouldInformTheDelegateWhenMappingBegins { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"users.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [[mockDelegate expect] objectMapperWillBeginMapping:mapper]; mapper.delegate = mockDelegate; @@ -742,11 +626,11 @@ - (void)testShouldInformTheDelegateWhenMappingBegins { - (void)testShouldInformTheDelegateWhenMappingEnds { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"users.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [[mockDelegate stub] objectMapperWillBeginMapping:mapper]; [[mockDelegate expect] objectMapperDidFinishMapping:mapper]; @@ -757,11 +641,11 @@ - (void)testShouldInformTheDelegateWhenMappingEnds { - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsSuccessful { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [[mockDelegate expect] objectMapper:mapper didFindMappableObject:[OCMArg any] atKeyPath:@""withMapping:mapping]; mapper.delegate = mockDelegate; @@ -771,10 +655,10 @@ - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsSucce - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsNotSuccessful { RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [provider setMapping:mapping forKeyPath:@"users"]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; [[mockDelegate expect] objectMapper:mapper didNotFindMappableObjectAtKeyPath:@"users"]; @@ -786,8 +670,8 @@ - (void)testShouldInformTheDelegateWhenCheckingForObjectMappingForKeyPathIsNotSu - (void)testShouldInformTheDelegateOfError { id mockProvider = [OCMockObject niceMockForClass:[RKObjectMappingProvider class]]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - - id userInfo = RKSpecParseFixture(@"users.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"users.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; [[mockDelegate expect] objectMapper:mapper didAddError:[OCMArg isNotNil]]; mapper.delegate = mockDelegate; @@ -797,11 +681,11 @@ - (void)testShouldInformTheDelegateOfError { - (void)testShouldNotifyTheDelegateWhenItWillMapAnObject { RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [provider setMapping:mapping forKeyPath:@""]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [[mockDelegate expect] objectMapper:mapper willMapFromObject:userInfo toObject:[OCMArg any] atKeyPath:@"" usingMapping:mapping]; mapper.delegate = mockDelegate; @@ -811,13 +695,13 @@ - (void)testShouldNotifyTheDelegateWhenItWillMapAnObject { - (void)testShouldNotifyTheDelegateWhenItDidMapAnObject { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; [[mockDelegate expect] objectMapper:mapper didMapFromObject:userInfo toObject:[OCMArg any] atKeyPath:@"" usingMapping:mapping]; mapper.delegate = mockDelegate; @@ -825,23 +709,23 @@ - (void)testShouldNotifyTheDelegateWhenItDidMapAnObject { [mockDelegate verify]; } -- (BOOL)fakeValidateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError { - *outError = [NSError errorWithDomain:RKRestKitErrorDomain code:1234 userInfo:nil]; +- (BOOL)fakeValidateValue:(inout id *)ioValue forKeyPath:(NSString *)inKey error:(out NSError **)outError { + *outError = [NSError errorWithDomain:RKErrorDomain code:1234 userInfo:nil]; return NO; } -- (void)testShouldNotifyTheDelegateWhenItFailedToMapAnObject { +- (void)testShouldNotifyTheDelegateWhenItFailedToMapAnObject { id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKObjectMapperDelegate)]; RKObjectMapping* mapping = [RKObjectMapping mappingForClass:NSClassFromString(@"OCPartialMockObject")]; [mapping mapAttributes:@"name", nil]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:mapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"user.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; - RKExampleUser* exampleUser = [[RKExampleUser new] autorelease]; + RKTestUser* exampleUser = [[RKTestUser new] autorelease]; id mockObject = [OCMockObject partialMockForObject:exampleUser]; - [[[mockObject expect] andCall:@selector(fakeValidateValue:forKey:error:) onObject:self] validateValue:[OCMArg anyPointer] forKey:OCMOCK_ANY error:[OCMArg anyPointer]]; + [[[mockObject expect] andCall:@selector(fakeValidateValue:forKeyPath:error:) onObject:self] validateValue:[OCMArg anyPointer] forKeyPath:OCMOCK_ANY error:[OCMArg anyPointer]]; mapper.targetObject = mockObject; [[mockDelegate expect] objectMapper:mapper didFailMappingFromObject:userInfo toObject:[OCMArg any] withError:[OCMArg any] atKeyPath:@"" usingMapping:mapping]; mapper.delegate = mockDelegate; @@ -850,7 +734,7 @@ - (void)testShouldNotifyTheDelegateWhenItFailedToMapAnObject { [mockDelegate verify]; } -#pragma mark - RKObjectMappingOperationSpecs +#pragma mark - RKObjectMappingOperationTests - (void)testShouldBeAbleToMapADictionaryToAUser { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; @@ -858,14 +742,14 @@ - (void)testShouldBeAbleToMapADictionaryToAUser { [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + NSMutableDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:123], @"id", @"Blake Watters", @"name", nil]; - RKExampleUser* user = [RKExampleUser user]; - + RKTestUser* user = [RKTestUser user]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; - [operation performMapping:nil]; + [operation performMapping:nil]; assertThat(user.name, is(equalTo(@"Blake Watters"))); - assertThatInt([user.userID intValue], is(equalToInt(123))); + assertThatInt([user.userID intValue], is(equalToInt(123))); [operation release]; } @@ -875,10 +759,10 @@ - (void)testShouldConsiderADictionaryContainingOnlyNullValuesForKeysMappable { [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + NSMutableDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"name", nil]; - RKExampleUser* user = [RKExampleUser user]; - + RKTestUser* user = [RKTestUser user]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -892,11 +776,11 @@ - (void)testShouldBeAbleToMapAUserToADictionary { [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - RKExampleUser* user = [RKExampleUser user]; + + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; user.userID = [NSNumber numberWithInt:123]; - + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:user destinationObject:dictionary mapping:mapping]; BOOL success = [operation performMapping:nil]; @@ -912,10 +796,10 @@ - (void)testShouldReturnNoWithoutErrorWhenGivenASourceObjectThatContainsNoMappab [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + NSMutableDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"blue", @"favorite_color", @"coffee", @"preferred_beverage", nil]; - RKExampleUser* user = [RKExampleUser user]; - + RKTestUser* user = [RKTestUser user]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; BOOL success = [operation performMapping:&error]; @@ -931,10 +815,10 @@ - (void)testShouldInformTheDelegateOfAnErrorWhenMappingFailsBecauseThereIsNoMapp [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + NSMutableDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"blue", @"favorite_color", @"coffee", @"preferred_beverage", nil]; - RKExampleUser* user = [RKExampleUser user]; - + RKTestUser* user = [RKTestUser user]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; operation.delegate = mockDelegate; BOOL success = [operation performMapping:nil]; @@ -948,12 +832,12 @@ - (void)testShouldSetTheErrorWhenMappingOperationFails { [mapping addAttributeMapping:idMapping]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - + NSMutableDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"FAILURE", @"id", nil]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; id mockObject = [OCMockObject partialMockForObject:user]; - [[[mockObject expect] andCall:@selector(fakeValidateValue:forKey:error:) onObject:self] validateValue:[OCMArg anyPointer] forKey:OCMOCK_ANY error:[OCMArg anyPointer]]; - + [[[mockObject expect] andCall:@selector(fakeValidateValue:forKeyPath:error:) onObject:self] validateValue:[OCMArg anyPointer] forKeyPath:OCMOCK_ANY error:[OCMArg anyPointer]]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:mockObject mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; @@ -966,16 +850,16 @@ - (void)testShouldSetTheErrorWhenMappingOperationFails { - (void)testShouldMapAStringToADateAttribute { [RKObjectMapping setDefaultDateFormatters:nil]; - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* birthDateMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"birthdate" toKeyPath:@"birthDate"]; [mapping addAttributeMapping:birthDateMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + NSDateFormatter* dateFormatter = [[NSDateFormatter new] autorelease]; dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; [dateFormatter setDateFormat:@"MM/dd/yyyy"]; @@ -983,150 +867,150 @@ - (void)testShouldMapAStringToADateAttribute { } - (void)testShouldMapStringToURL { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"website" toKeyPath:@"website"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + assertThat(user.website, isNot(nilValue())); assertThatBool([user.website isKindOfClass:[NSURL class]], is(equalToBool(YES))); assertThat([user.website absoluteString], is(equalTo(@"http://restkit.org/"))); } - (void)testShouldMapAStringToANumberBool { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"is_developer" toKeyPath:@"isDeveloper"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); + + assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); } - (void)testShouldMapAShortTrueStringToANumberBool { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"is_developer" toKeyPath:@"isDeveloper"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + RKTestUser* user = [RKTestUser user]; [dictionary setValue:@"T" forKey:@"is_developer"]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); + + assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); } - (void)testShouldMapAShortFalseStringToANumberBool { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"is_developer" toKeyPath:@"isDeveloper"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + RKTestUser* user = [RKTestUser user]; [dictionary setValue:@"f" forKey:@"is_developer"]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThatBool([[user isDeveloper] boolValue], is(equalToBool(NO))); + + assertThatBool([[user isDeveloper] boolValue], is(equalToBool(NO))); } - (void)testShouldMapAYesStringToANumberBool { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"is_developer" toKeyPath:@"isDeveloper"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + RKTestUser* user = [RKTestUser user]; [dictionary setValue:@"yes" forKey:@"is_developer"]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); + + assertThatBool([[user isDeveloper] boolValue], is(equalToBool(YES))); } - (void)testShouldMapANoStringToANumberBool { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"is_developer" toKeyPath:@"isDeveloper"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + RKTestUser* user = [RKTestUser user]; [dictionary setValue:@"NO" forKey:@"is_developer"]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThatBool([[user isDeveloper] boolValue], is(equalToBool(NO))); + + assertThatBool([[user isDeveloper] boolValue], is(equalToBool(NO))); } - (void)testShouldMapAStringToANumber { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"lucky_number" toKeyPath:@"luckyNumber"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + assertThatInt([user.luckyNumber intValue], is(equalToInt(187))); } - (void)testShouldMapAStringToADecimalNumber { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"weight" toKeyPath:@"weight"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + NSDecimalNumber* weight = user.weight; assertThatBool([weight isKindOfClass:[NSDecimalNumber class]], is(equalToBool(YES))); assertThatInteger([weight compare:[NSDecimalNumber decimalNumberWithString:@"131.3"]], is(equalToInt(NSOrderedSame))); } - (void)testShouldMapANumberToAString { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"lucky_number" toKeyPath:@"name"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - - assertThat(user.name, is(equalTo(@"187"))); + + assertThat(user.name, is(equalTo(@"187"))); } - (void)testShouldMapANumberToANSDecimalNumber { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* websiteMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"lucky_number" toKeyPath:@"weight"]; [mapping addAttributeMapping:websiteMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + NSDecimalNumber* weight = user.weight; assertThatBool([weight isKindOfClass:[NSDecimalNumber class]], is(equalToBool(YES))); assertThatInteger([weight compare:[NSDecimalNumber decimalNumberWithString:@"187"]], is(equalToInt(NSOrderedSame))); @@ -1136,61 +1020,61 @@ - (void)testShouldMapANumberToADate { NSDateFormatter* dateFormatter = [[NSDateFormatter new] autorelease]; [dateFormatter setDateFormat:@"MM/dd/yyyy"]; NSDate* date = [dateFormatter dateFromString:@"11/27/1982"]; - - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* birthDateMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"dateAsNumber" toKeyPath:@"birthDate"]; [mapping addAttributeMapping:birthDateMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; [dictionary setValue:[NSNumber numberWithInt:[date timeIntervalSince1970]] forKey:@"dateAsNumber"]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + assertThat([dateFormatter stringFromDate:user.birthDate], is(equalTo(@"11/27/1982"))); } - (void)testShouldMapANestedKeyPathToAnAttribute { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* countryMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"address.country" toKeyPath:@"country"]; [mapping addAttributeMapping:countryMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + assertThat(user.country, is(equalTo(@"USA"))); } - (void)testShouldMapANestedArrayOfStringsToAnAttribute { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* countryMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"interests" toKeyPath:@"interests"]; [mapping addAttributeMapping:countryMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + NSArray* interests = [NSArray arrayWithObjects:@"Hacking", @"Running", nil]; assertThat(user.interests, is(equalTo(interests))); } - (void)testShouldMapANestedDictionaryToAnAttribute { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* countryMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"address" toKeyPath:@"addressDictionary"]; [mapping addAttributeMapping:countryMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:user mapping:mapping]; NSError* error = nil; [operation performMapping:&error]; - + NSDictionary* address = [NSDictionary dictionaryWithKeysAndObjects: @"city", @"Carrboro", @"state", @"North Carolina", @@ -1200,12 +1084,12 @@ - (void)testShouldMapANestedDictionaryToAnAttribute { } - (void)testShouldNotSetAPropertyWhenTheValueIsTheSame { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - NSDictionary* dictionary = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + + NSDictionary* dictionary = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setName:OCMOCK_ANY]; @@ -1215,13 +1099,13 @@ - (void)testShouldNotSetAPropertyWhenTheValueIsTheSame { } - (void)testShouldNotSetTheDestinationPropertyWhenBothAreNil { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; [dictionary setValue:[NSNull null] forKey:@"name"]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; user.name = nil; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setName:OCMOCK_ANY]; @@ -1231,13 +1115,13 @@ - (void)testShouldNotSetTheDestinationPropertyWhenBothAreNil { } - (void)testShouldSetNilForNSNullValues { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - NSDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; + + NSDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; [dictionary setValue:[NSNull null] forKey:@"name"]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser expect] setName:nil]; @@ -1247,14 +1131,52 @@ - (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:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; [dictionary removeObjectForKey:@"name"]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser expect] setName:nil]; @@ -1268,13 +1190,13 @@ - (void)testShouldOptionallySetDefaultValueForAMissingKeyPath { } - (void)testShouldOptionallyIgnoreAMissingSourceKeyPath { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [mapping addAttributeMapping:nameMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; [dictionary removeObjectForKey:@"name"]; - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; user.name = @"Blake Watters"; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setName:nil]; @@ -1290,19 +1212,19 @@ - (void)testShouldOptionallyIgnoreAMissingSourceKeyPath { #pragma mark - Relationship Mapping - (void)testShouldMapANestedObject { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; RKObjectAttributeMapping* cityMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"city" toKeyPath:@"city"]; [addressMapping addAttributeMapping:cityMapping]; - + RKObjectRelationshipMapping* hasOneMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; [userMapping addRelationshipMapping:hasOneMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; [mapper release]; assertThatBool(success, is(equalToBool(YES))); @@ -1311,19 +1233,19 @@ - (void)testShouldMapANestedObject { } - (void)testShouldMapANestedObjectToCollection { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; RKObjectAttributeMapping* cityMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"city" toKeyPath:@"city"]; [addressMapping addAttributeMapping:cityMapping]; - + RKObjectRelationshipMapping* hasOneMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"friends" withMapping:addressMapping]; [userMapping addRelationshipMapping:hasOneMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; [mapper release]; assertThatBool(success, is(equalToBool(YES))); @@ -1332,17 +1254,39 @@ - (void)testShouldMapANestedObjectToCollection { assertThatUnsignedInteger([user.friends count], is(equalToInt(1))); } +- (void)testShouldMapANestedObjectToOrderedSetCollection { + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; + [userMapping addAttributeMapping:nameMapping]; + 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]; + BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + [mapper release]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(user.name, is(equalTo(@"Blake Watters"))); + assertThat(user.friendsOrderedSet, isNot(nilValue())); + assertThatUnsignedInteger([user.friendsOrderedSet count], is(equalToInt(1))); +} + - (void)testShouldMapANestedObjectCollection { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - + RKObjectRelationshipMapping* hasManyMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:userMapping]; [userMapping addRelationshipMapping:hasManyMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; [mapper release]; assertThatBool(success, is(equalToBool(YES))); @@ -1354,16 +1298,16 @@ - (void)testShouldMapANestedObjectCollection { } - (void)testShouldMapANestedArrayIntoASet { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - + RKObjectRelationshipMapping* hasManyMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"friends" toKeyPath:@"friendsSet" withMapping:userMapping]; [userMapping addRelationshipMapping:hasManyMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; [mapper release]; assertThatBool(success, is(equalToBool(YES))); @@ -1375,53 +1319,102 @@ - (void)testShouldMapANestedArrayIntoASet { assertThat([user.friendsSet valueForKey:@"name"], is(equalTo(names))); } +- (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]; + BOOL success = [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; + [mapper release]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(user.name, is(equalTo(@"Blake Watters"))); + assertThat(user.friendsOrderedSet, isNot(nilValue())); + assertThatBool([user.friendsOrderedSet isKindOfClass:[NSOrderedSet class]], is(equalToBool(YES))); + assertThatUnsignedInteger([user.friendsOrderedSet count], is(equalToInt(2))); + NSOrderedSet *names = [NSOrderedSet orderedSetWithObjects:@"Jeremy Ellison", @"Rachit Shukla", nil]; + assertThat([user.friendsOrderedSet valueForKey:@"name"], is(equalTo(names))); +} + - (void)testShouldNotSetThePropertyWhenTheNestedObjectIsIdentical { - RKExampleUser* user = [RKExampleUser user]; - RKSpecAddress* address = [RKSpecAddress address]; + 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:[RKExampleUser class]]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + 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]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; [mapper mapFromObject:userInfo toObject:user atKeyPath:@"" usingMapping:userMapping]; [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:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"userID"]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:idMapping]; [userMapping addAttributeMapping:nameMapping]; - + RKObjectRelationshipMapping* hasManyMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:userMapping]; [userMapping addRelationshipMapping:hasManyMapping]; - + RKObjectMapper* mapper = [RKObjectMapper new]; - id userInfo = RKSpecParseFixture(@"user.json"); - RKExampleUser* user = [RKExampleUser user]; - + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"]; + RKTestUser* user = [RKTestUser user]; + // Set the friends up - RKExampleUser* jeremy = [RKExampleUser user]; + RKTestUser* jeremy = [RKTestUser user]; jeremy.name = @"Jeremy Ellison"; jeremy.userID = [NSNumber numberWithInt:187]; - RKExampleUser* rachit = [RKExampleUser user]; - rachit.name = @"Rachit Shukla"; + RKTestUser* rachit = [RKTestUser user]; + rachit.name = @"Rachit Shukla"; rachit.userID = [NSNumber numberWithInt:7]; user.friends = [NSArray arrayWithObjects:jeremy, rachit, nil]; - + id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setFriends:OCMOCK_ANY]; [mapper mapFromObject:userInfo toObject:mockUser atKeyPath:@"" usingMapping:userMapping]; @@ -1430,56 +1423,56 @@ - (void)testShouldNotSetThePropertyWhenTheNestedObjectCollectionIsIdentical { } - (void)testShouldOptionallyNilOutTheRelationshipIfItIsMissing { - RKExampleUser* user = [RKExampleUser user]; - RKSpecAddress* address = [RKSpecAddress address]; + RKTestUser* user = [RKTestUser user]; + RKTestAddress* address = [RKTestAddress address]; address.addressID = [NSNumber numberWithInt:1234]; user.address = address; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser expect] setAddress:nil]; - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"addressID"]; [addressMapping addAttributeMapping:idMapping]; RKObjectRelationshipMapping* relationshipMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; [userMapping addRelationshipMapping:relationshipMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - [dictionary removeObjectForKey:@"address"]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + [dictionary removeObjectForKey:@"address"]; id mockMapping = [OCMockObject partialMockForObject:userMapping]; BOOL returnValue = YES; [[[mockMapping expect] andReturnValue:OCMOCK_VALUE(returnValue)] setNilForMissingRelationships]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:mockUser mapping:mockMapping]; - + NSError* error = nil; [operation performMapping:&error]; [mockUser verify]; } - (void)testShouldNotNilOutTheRelationshipIfItIsMissingAndCurrentlyNilOnTheTargetObject { - RKExampleUser* user = [RKExampleUser user]; + RKTestUser* user = [RKTestUser user]; user.address = nil; id mockUser = [OCMockObject partialMockForObject:user]; [[mockUser reject] setAddress:nil]; - - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectAttributeMapping* nameMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"name" toKeyPath:@"name"]; [userMapping addAttributeMapping:nameMapping]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; RKObjectAttributeMapping* idMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"addressID"]; [addressMapping addAttributeMapping:idMapping]; RKObjectRelationshipMapping* relationshipMapping = [RKObjectRelationshipMapping mappingFromKeyPath:@"address" toKeyPath:@"address" withMapping:addressMapping]; [userMapping addRelationshipMapping:relationshipMapping]; - - NSMutableDictionary* dictionary = [RKSpecParseFixture(@"user.json") mutableCopy]; - [dictionary removeObjectForKey:@"address"]; + + NSMutableDictionary* dictionary = [[RKTestFixture parsedObjectWithContentsOfFixture:@"user.json"] mutableCopy]; + [dictionary removeObjectForKey:@"address"]; id mockMapping = [OCMockObject partialMockForObject:userMapping]; BOOL returnValue = YES; [[[mockMapping expect] andReturnValue:OCMOCK_VALUE(returnValue)] setNilForMissingRelationships]; RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:mockUser mapping:mockMapping]; - + NSError* error = nil; [operation performMapping:&error]; [mockUser verify]; @@ -1487,43 +1480,42 @@ - (void)testShouldNotNilOutTheRelationshipIfItIsMissingAndCurrentlyNilOnTheTarge #pragma mark - RKObjectMappingProvider -- (void)testShouldRegisterRailsIdiomaticObjects { - RKObjectManager* objectManager = RKSpecNewObjectManager(); - RKSpecStubNetworkAvailability(YES); - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; +- (void)testShouldRegisterRailsIdiomaticObjects { + RKObjectManager* objectManager = [RKTestFactory objectManager]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [mapping mapAttributes:@"name", @"website", nil]; [mapping mapKeyPath:@"id" toAttribute:@"userID"]; - - [objectManager.router routeClass:[RKExampleUser class] toResourcePath:@"/humans/:userID"]; - [objectManager.router routeClass:[RKExampleUser class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST]; + + [objectManager.router routeClass:[RKTestUser class] toResourcePath:@"/humans/:userID"]; + [objectManager.router routeClass:[RKTestUser class] toResourcePath:@"/humans" forMethod:RKRequestMethodPOST]; [objectManager.mappingProvider registerMapping:mapping withRootKeyPath:@"human"]; - - RKExampleUser* user = [RKExampleUser new]; + + RKTestUser* user = [RKTestUser new]; user.userID = [NSNumber numberWithInt:1]; - - RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader]; + + RKTestResponseLoader* loader = [RKTestResponseLoader responseLoader]; loader.timeout = 5; [objectManager getObject:user delegate:loader]; [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(YES))); + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"Blake Watters"))); - + [objectManager postObject:user delegate:loader]; [loader waitForResponse]; - assertThatBool(loader.success, is(equalToBool(YES))); + assertThatBool(loader.wasSuccessful, is(equalToBool(YES))); assertThat(user.name, is(equalTo(@"My Name"))); assertThat(user.website, is(equalTo([NSURL URLWithString:@"http://restkit.org/"]))); } - (void)testShouldReturnAllMappingsForAClass { - RKObjectMapping* firstMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; - RKObjectMapping* secondMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; - RKObjectMapping* thirdMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* firstMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping* secondMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping* thirdMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; RKObjectMappingProvider* mappingProvider = [[RKObjectMappingProvider new] autorelease]; [mappingProvider addObjectMapping:firstMapping]; [mappingProvider addObjectMapping:secondMapping]; [mappingProvider setMapping:thirdMapping forKeyPath:@"third"]; - assertThat([mappingProvider objectMappingsForClass:[RKExampleUser class]], is(equalTo([NSArray arrayWithObjects:firstMapping, secondMapping, thirdMapping, nil]))); + assertThat([mappingProvider objectMappingsForClass:[RKTestUser class]], is(equalTo([NSArray arrayWithObjects:firstMapping, secondMapping, thirdMapping, nil]))); } - (void)testShouldReturnAllMappingsForAClassAndNotExplodeWithRegisteredDynamicMappings { @@ -1536,14 +1528,14 @@ - (void)testShouldReturnAllMappingsForAClassAndNotExplodeWithRegisteredDynamicMa [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; [provider setMapping:dynamicMapping forKeyPath:@"dynamic"]; - RKObjectMapping* firstMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; - RKObjectMapping* secondMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* firstMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; + RKObjectMapping* secondMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [provider addObjectMapping:firstMapping]; [provider setMapping:secondMapping forKeyPath:@"second"]; NSException* exception = nil; NSArray *actualMappings = nil; @try { - actualMappings = [provider objectMappingsForClass:[RKExampleUser class]]; + actualMappings = [provider objectMappingsForClass:[RKTestUser class]]; } @catch (NSException * e) { exception = e; @@ -1566,15 +1558,15 @@ - (void)testShouldMapASingleObjectDynamically { } else if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { return girlMapping; } - + return nil; }; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"boy.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; Boy* user = [[mapper performMapping] asObject]; assertThat(user, is(instanceOf([Boy class]))); @@ -1589,12 +1581,12 @@ - (void)testShouldMapASingleObjectDynamicallyWithADeclarativeMatcher { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"boy.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; Boy* user = [[mapper performMapping] asObject]; assertThat(user, is(instanceOf([Boy class]))); @@ -1609,12 +1601,12 @@ - (void)testShouldACollectionOfObjectsDynamically { RKDynamicObjectMapping* dynamicMapping = [RKDynamicObjectMapping dynamicMapping]; [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"mixed.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"mixed.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; NSArray* objects = [[mapper performMapping] asCollection]; assertThat(objects, hasCountOf(2)); @@ -1635,16 +1627,16 @@ - (void)testShouldMapARelationshipDynamically { [dynamicMapping setObjectMapping:boyMapping whenValueOfKeyPath:@"type" isEqualTo:@"Boy"]; [dynamicMapping setObjectMapping:girlMapping whenValueOfKeyPath:@"type" isEqualTo:@"Girl"]; [boyMapping mapKeyPath:@"friends" toRelationship:@"friends" withMapping:dynamicMapping]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"friends.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"friends.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; Boy* blake = [[mapper performMapping] asObject]; NSArray* friends = blake.friends; - + assertThat(friends, hasCountOf(2)); assertThat([friends objectAtIndex:0], is(instanceOf([Boy class]))); assertThat([friends objectAtIndex:1], is(instanceOf([Girl class]))); @@ -1667,15 +1659,15 @@ - (void)testShouldBeAbleToDeclineMappingAnObjectByReturningANilObjectMapping { // NO GIRLS ALLOWED(*$!)(* return nil; } - + return nil; }; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"mixed.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"mixed.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; NSArray* boys = [[mapper performMapping] asCollection]; assertThat(boys, hasCountOf(1)); @@ -1697,23 +1689,23 @@ - (void)testShouldBeAbleToDeclineMappingObjectsInARelationshipByReturningANilObj // NO GIRLS ALLOWED(*$!)(* return nil; } - + return nil; }; [boyMapping mapKeyPath:@"friends" toRelationship:@"friends" withMapping:dynamicMapping]; - + RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease]; [provider setMapping:dynamicMapping forKeyPath:@""]; id mockProvider = [OCMockObject partialMockForObject:provider]; - - id userInfo = RKSpecParseFixture(@"friends.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"friends.json"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:mockProvider]; Boy* blake = [[mapper performMapping] asObject]; assertThat(blake, is(notNilValue())); assertThat(blake.name, is(equalTo(@"Blake Watters"))); assertThat(blake, is(instanceOf([Boy class]))); NSArray* friends = blake.friends; - + assertThat(friends, hasCountOf(1)); assertThat([friends objectAtIndex:0], is(instanceOf([Boy class]))); Boy* boy = [friends objectAtIndex:0]; @@ -1728,14 +1720,14 @@ - (void)testShouldMapATargetObjectWithADynamicMapping { if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; } - + return nil; }; - + RKObjectMappingProvider* provider = [RKObjectMappingProvider objectMappingProvider]; [provider setMapping:dynamicMapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"boy.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; Boy* blake = [[Boy new] autorelease]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; mapper.targetObject = blake; @@ -1752,14 +1744,14 @@ - (void)testShouldBeBackwardsCompatibleWithTheOldClassName { if ([[mappableData valueForKey:@"type"] isEqualToString:@"Boy"]) { return boyMapping; } - + return nil; }; - + RKObjectMappingProvider* provider = [RKObjectMappingProvider objectMappingProvider]; [provider setMapping:dynamicMapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"boy.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; Boy* blake = [[Boy new] autorelease]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; mapper.targetObject = blake; @@ -1775,11 +1767,11 @@ - (void)testShouldFailWithAnErrorIfATargetObjectIsProvidedAndTheDynamicMappingRe dynamicMapping.objectMappingForDataBlock = ^ RKObjectMapping* (id mappableData) { return nil; }; - + RKObjectMappingProvider* provider = [RKObjectMappingProvider objectMappingProvider]; [provider setMapping:dynamicMapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"boy.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"boy.json"]; Boy* blake = [[Boy new] autorelease]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; mapper.targetObject = blake; @@ -1796,14 +1788,14 @@ - (void)testShouldFailWithAnErrorIfATargetObjectIsProvidedAndTheDynamicMappingRe if ([[mappableData valueForKey:@"type"] isEqualToString:@"Girl"]) { return girlMapping; } - + return nil; }; - + RKObjectMappingProvider* provider = [RKObjectMappingProvider objectMappingProvider]; [provider setMapping:dynamicMapping forKeyPath:@""]; - - id userInfo = RKSpecParseFixture(@"girl.json"); + + id userInfo = [RKTestFixture parsedObjectWithContentsOfFixture:@"girl.json"]; Boy* blake = [[Boy new] autorelease]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:userInfo mappingProvider:provider]; mapper.targetObject = blake; @@ -1817,7 +1809,7 @@ - (void)testShouldFailWithAnErrorIfATargetObjectIsProvidedAndTheDynamicMappingRe - (void)testShouldAutoConfigureDefaultDateFormatters { [RKObjectMapping setDefaultDateFormatters:nil]; NSArray *dateFormatters = [RKObjectMapping defaultDateFormatters]; - assertThat(dateFormatters, hasCountOf(2)); + assertThat(dateFormatters, hasCountOf(3)); assertThat([[dateFormatters objectAtIndex:0] dateFormat], is(equalTo(@"yyyy-MM-dd'T'HH:mm:ss'Z'"))); assertThat([[dateFormatters objectAtIndex:1] dateFormat], is(equalTo(@"MM/dd/yyyy"))); NSTimeZone *UTCTimeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; @@ -1834,48 +1826,92 @@ - (void)testShouldLetYouSetTheDefaultDateFormatters { - (void)testShouldLetYouAppendADateFormatterToTheList { [RKObjectMapping setDefaultDateFormatters:nil]; - assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(2)); + assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(3)); NSDateFormatter *dateFormatter = [NSDateFormatter new]; [RKObjectMapping addDefaultDateFormatter:dateFormatter]; - assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(3)); + assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(4)); +} + +- (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]; + NSError *error = nil; + [operation performMapping:&error]; + + NSDateFormatter *dateFormatter = [[NSDateFormatter new] autorelease]; + dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + [dateFormatter setDateFormat:@"MM/dd/yyyy"]; + + /* + If RKObjectMappingOperation is using the date formatter set above, we're + going to get a really wonky date, which is what we are testing for. + */ + assertThat([dateFormatter stringFromDate:user.favoriteDate], is(equalTo(@"01/03/2012"))); } - (void)testShouldLetYouConfigureANewDateFormatterFromAStringAndATimeZone { [RKObjectMapping setDefaultDateFormatters:nil]; - assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(2)); + assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(3)); NSTimeZone *EDTTimeZone = [NSTimeZone timeZoneWithAbbreviation:@"EDT"]; [RKObjectMapping addDefaultDateFormatterForString:@"mm/dd/YYYY" inTimeZone:EDTTimeZone]; - assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(3)); - NSDateFormatter *dateFormatter = [[RKObjectMapping defaultDateFormatters] objectAtIndex:2]; + assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(4)); + NSDateFormatter *dateFormatter = [[RKObjectMapping defaultDateFormatters] objectAtIndex:0]; assertThat(dateFormatter.timeZone, is(equalTo(EDTTimeZone))); } +- (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"]; + RKTestUser* user = [RKTestUser user]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:mutableDictionary destinationObject:user mapping:mapping]; + [mutableDictionary release]; + NSError* error = nil; + [operation performMapping:&error]; + + assertThat(user.birthDate, is(equalTo(nil))); +} + - (void)testShouldConfigureANewDateFormatterInTheUTCTimeZoneIfPassedANilTimeZone { [RKObjectMapping setDefaultDateFormatters:nil]; - assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(2)); - [RKObjectMapping addDefaultDateFormatterForString:@"mm/dd/YYYY" inTimeZone:nil]; assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(3)); - NSDateFormatter *dateFormatter = [[RKObjectMapping defaultDateFormatters] objectAtIndex:2]; + [RKObjectMapping addDefaultDateFormatterForString:@"mm/dd/YYYY" inTimeZone:nil]; + assertThat([RKObjectMapping defaultDateFormatters], hasCountOf(4)); + NSDateFormatter *dateFormatter = [[RKObjectMapping defaultDateFormatters] objectAtIndex:0]; NSTimeZone *UTCTimeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; assertThat(dateFormatter.timeZone, is(equalTo(UTCTimeZone))); } #pragma mark - Object Serialization -// TODO: Move to RKObjectSerializerSpec +// TODO: Move to RKObjectSerializerTest - (void)testShouldSerializeHasOneRelatioshipsToJSON { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [userMapping mapAttributes:@"name", nil]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [addressMapping mapAttributes:@"city", @"state", nil]; [userMapping hasOne:@"address" withMapping:addressMapping]; - - RKExampleUser *user = [RKExampleUser new]; + + RKTestUser *user = [RKTestUser new]; user.name = @"Blake Watters"; - RKSpecAddress *address = [RKSpecAddress new]; + RKTestAddress *address = [RKTestAddress new]; address.state = @"North Carolina"; user.address = address; - + RKObjectMapping *serializationMapping = [userMapping inverseMapping]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:user mapping:serializationMapping]; NSError* error = nil; @@ -1885,21 +1921,21 @@ - (void)testShouldSerializeHasOneRelatioshipsToJSON { } - (void)testShouldSerializeHasManyRelationshipsToJSON { - RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKExampleUser class]]; + RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[RKTestUser class]]; [userMapping mapAttributes:@"name", nil]; - RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKSpecAddress class]]; + RKObjectMapping* addressMapping = [RKObjectMapping mappingForClass:[RKTestAddress class]]; [addressMapping mapAttributes:@"city", @"state", nil]; [userMapping hasMany:@"friends" withMapping:addressMapping]; - - RKExampleUser *user = [RKExampleUser new]; + + RKTestUser *user = [RKTestUser new]; user.name = @"Blake Watters"; - RKSpecAddress *address1 = [RKSpecAddress new]; + RKTestAddress *address1 = [RKTestAddress new]; address1.city = @"Carrboro"; - RKSpecAddress *address2 = [RKSpecAddress new]; + RKTestAddress *address2 = [RKTestAddress new]; address2.city = @"New York City"; user.friends = [NSArray arrayWithObjects:address1, address2, nil]; - - + + RKObjectMapping *serializationMapping = [userMapping inverseMapping]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:user mapping:serializationMapping]; NSError* error = nil; @@ -1909,21 +1945,21 @@ - (void)testShouldSerializeHasManyRelationshipsToJSON { } - (void)testShouldSerializeManagedHasManyRelationshipsToJSON { - RKSpecNewManagedObjectStore(); + [RKTestFactory managedObjectStore]; RKObjectMapping* humanMapping = [RKObjectMapping mappingForClass:[RKHuman class]]; [humanMapping mapAttributes:@"name", nil]; RKObjectMapping* catMapping = [RKObjectMapping mappingForClass:[RKCat class]]; [catMapping mapAttributes:@"name", nil]; [humanMapping hasMany:@"cats" withMapping:catMapping]; - + RKHuman *blake = [RKHuman object]; blake.name = @"Blake Watters"; RKCat *asia = [RKCat object]; asia.name = @"Asia"; RKCat *roy = [RKCat object]; roy.name = @"Roy"; - blake.cats = [NSSet setWithObjects:asia, roy, nil]; - + blake.cats = [NSSet setWithObjects:asia, roy, nil]; + RKObjectMapping *serializationMapping = [humanMapping inverseMapping]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:blake mapping:serializationMapping]; NSError* error = nil; @@ -1935,4 +1971,30 @@ - (void)testShouldSerializeManagedHasManyRelationshipsToJSON { assertThat(catNames, is(equalTo([NSArray arrayWithObjects:@"Asia", @"Roy", nil]))); } +- (void)testUpdatingArrayOfExistingCats { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + NSArray *array = [RKTestFixture parsedObjectWithContentsOfFixture:@"ArrayOfHumans.json"]; + RKManagedObjectMapping *humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; + [humanMapping mapKeyPath:@"id" toAttribute:@"railsID"]; + 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 objectAtIndex:0], is(equalTo(human1))); + assertThat([humans objectAtIndex:1], is(equalTo(human2))); +} + @end diff --git a/Specs/ObjectMapping/RKObjectMappingOperationSpec.m b/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m similarity index 70% rename from Specs/ObjectMapping/RKObjectMappingOperationSpec.m rename to Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m index cb66438095..1a7c5209ff 100644 --- a/Specs/ObjectMapping/RKObjectMappingOperationSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingOperationTest.m @@ -1,16 +1,16 @@ // -// RKObjectMappingOperationSpec.m +// RKObjectMappingOperationTest.m // RestKit // // Created by Blake Watters on 4/30/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,7 +18,7 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKObjectMapperError.h" #import "RKMappableObject.h" #import "RKMappableAssociation.h" @@ -51,24 +51,27 @@ @implementation TestMappable - (BOOL)validateBoolString:(id *)ioValue error:(NSError **)outError { if ([(NSObject *)*ioValue isKindOfClass:[NSString class]] && [(NSString *)*ioValue isEqualToString:@"FAIL"]) { - *outError = [NSError errorWithDomain:RKRestKitErrorDomain code:RKObjectMapperErrorUnmappableContent userInfo:nil]; + *outError = [NSError errorWithDomain:RKErrorDomain code:RKObjectMapperErrorUnmappableContent userInfo:nil]; return NO; } else if ([(NSObject *)*ioValue isKindOfClass:[NSString class]] && [(NSString *)*ioValue isEqualToString:@"REJECT"]) { return NO; + } else if ([(NSObject *)*ioValue isKindOfClass:[NSString class]] && [(NSString *)*ioValue isEqualToString:@"MODIFY"]) { + *ioValue = @"modified value"; + return YES; } - + return YES; } @end -@interface RKObjectMappingOperationSpec : RKSpec { - +@interface RKObjectMappingOperationTest : RKTestCase { + } @end -@implementation RKObjectMappingOperationSpec +@implementation RKObjectMappingOperationTest - (void)testShouldNotUpdateEqualURLProperties { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; @@ -79,7 +82,7 @@ - (void)testShouldNotUpdateEqualURLProperties { TestMappable* object = [[[TestMappable alloc] init] autorelease]; [object setUrl:url1]; NSDictionary* dictionary = [NSDictionary dictionaryWithObjectsAndKeys:url2, @"url", nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -91,10 +94,10 @@ - (void)testShouldSuccessfullyMapBoolsToStrings { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapAttributes:@"boolString", nil]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:@"application/json"]; id data = [parser objectFromString:@"{\"boolString\":true}" error:nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -106,10 +109,10 @@ - (void)testShouldSuccessfullyMapTrueBoolsToNSNumbers { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapAttributes:@"boolNumber", nil]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:@"application/json"]; id data = [parser objectFromString:@"{\"boolNumber\":true}" error:nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -121,10 +124,10 @@ - (void)testShouldSuccessfullyMapFalseBoolsToNSNumbers { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapAttributes:@"boolNumber", nil]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:@"application/json"]; id data = [parser objectFromString:@"{\"boolNumber\":false}" error:nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -136,10 +139,10 @@ - (void)testShouldSuccessfullyMapNumbersToStrings { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapKeyPath:@"number" toAttribute:@"boolString"]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:@"application/json"]; id data = [parser objectFromString:@"{\"number\":123}" error:nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -151,10 +154,10 @@ - (void)testShouldSuccessfullyMapArraysToOrderedSets { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapKeyPath:@"numbers" toAttribute:@"orderedSet"]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:@"application/json"]; id data = [parser objectFromString:@"{\"numbers\":[1, 2, 3]}" error:nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -167,10 +170,10 @@ - (void)testShouldSuccessfullyMapOrderedSetsToArrays { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapKeyPath:@"orderedSet" toAttribute:@"array"]; TestMappable* object = [[[TestMappable alloc] init] autorelease]; - + TestMappable* data = [[[TestMappable alloc] init] autorelease]; data.orderedSet = [NSOrderedSet orderedSetWithObjects:[NSNumber numberWithInt:1], [NSNumber numberWithInt:2], [NSNumber numberWithInt:3], nil]; - + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:object mapping:mapping]; BOOL success = [operation performMapping:nil]; assertThatBool(success, is(equalToBool(YES))); @@ -206,6 +209,19 @@ - (void)testShouldNotSetTheAttributeIfKeyValueValidationReturnsNo { [operation release]; } +- (void)testModifyingValueWithinKeyValueValidationIsRespected { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; + [mapping mapAttributes:@"boolString", nil]; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:@"MODIFY" forKey:@"boolString"]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:object mapping:mapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(object.boolString, is(equalTo(@"modified value"))); + [operation release]; +} + #pragma mark - TimeZone Handling - (void)testShouldMapAUTCDateWithoutChangingTheTimeZone { @@ -222,6 +238,20 @@ - (void)testShouldMapAUTCDateWithoutChangingTheTimeZone { [operation release]; } +- (void)testShouldMapAUnixTimestampStringAppropriately { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; + [mapping mapAttributes:@"date", nil]; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:@"457574400" forKey:@"date"]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:object mapping:mapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(object.date, isNot(nilValue())); + assertThat([object.date description], is(equalTo(@"1984-07-02 00:00:00 +0000"))); + [operation release]; +} + - (void)testShouldMapASimpleDateStringAppropriately { RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapAttributes:@"date", nil]; @@ -236,6 +266,20 @@ - (void)testShouldMapASimpleDateStringAppropriately { [operation release]; } +- (void)testShouldMapAISODateStringAppropriately { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; + [mapping mapAttributes:@"date", nil]; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:@"2011-08-09T00:00Z" forKey:@"date"]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:dictionary destinationObject:object mapping:mapping]; + NSError* error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + assertThat(object.date, isNot(nilValue())); + assertThat([object.date description], is(equalTo(@"2011-08-09 00:00:00 +0000"))); + [operation release]; +} + - (void)testShouldMapAStringIntoTheLocalTimeZone { NSTimeZone *EDTTimeZone = [NSTimeZone timeZoneWithAbbreviation:@"EDT"]; NSDateFormatter *dateFormatter = [[NSDateFormatter new] autorelease]; @@ -257,7 +301,6 @@ - (void)testShouldMapAStringIntoTheLocalTimeZone { } - (void)testShouldMapADateToAStringUsingThePreferredDateFormatter { - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[TestMappable class]]; [mapping mapKeyPath:@"date" toAttribute:@"boolString"]; NSDateFormatter *dateFormatter = [[NSDateFormatter new] autorelease]; @@ -274,18 +317,90 @@ - (void)testShouldMapADateToAStringUsingThePreferredDateFormatter { assertThat(newObject.boolString, is(equalTo(@"11-27-1982"))); } +- (void)testShouldGenerateAnUnknownKeyPathExceptionWhenIgnoreUnknownKeyPathsIsNO { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [mapping mapAttributes:@"invalid", @"boolString", nil]; + mapping.ignoreUnknownKeyPaths = NO; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + object.boolString = @"test"; + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:object destinationObject:dictionary mapping:mapping]; + NSError* error = nil; + BOOL success; + NSException* exception = nil; + @try { + success = [operation performMapping:&error]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, isNot(nilValue())); + [operation release]; + } +} + +- (void)testShouldOptionallyIgnoreUnknownKeyPathAttributes { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [mapping mapAttributes:@"invalid", @"boolString", nil]; + mapping.ignoreUnknownKeyPaths = YES; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + object.boolString = @"test"; + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:object destinationObject:dictionary mapping:mapping]; + NSError* error = nil; + BOOL success; + NSException* exception = nil; + @try { + success = [operation performMapping:&error]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, is(nilValue())); + assertThatBool(success, is(equalToBool(YES))); + [operation release]; + } +} + +- (void)testShouldOptionallyIgnoreUnknownKeyPathRelationships { + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; + [mapping mapAttributes:@"boolString", nil]; + [mapping mapRelationship:@"invalid" withMapping:[RKObjectMapping mappingForClass:[TestMappable class]]]; + mapping.ignoreUnknownKeyPaths = YES; + TestMappable* object = [[[TestMappable alloc] init] autorelease]; + object.boolString = @"test"; + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + RKObjectMappingOperation* operation = [[RKObjectMappingOperation alloc] initWithSourceObject:object destinationObject:dictionary mapping:mapping]; + NSError* error = nil; + BOOL success; + NSException* exception = nil; + @try { + success = [operation performMapping:&error]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, is(nilValue())); + assertThatBool(success, is(equalToBool(YES))); + [operation release]; + } +} + - (void)testShouldLogADebugMessageIfTheRelationshipMappingTargetsAnArrayOfArrays { // Create a dictionary with a dictionary containing an array // Use keyPath to traverse to the collection and target a hasMany - id data = RKSpecParseFixture(@"ArrayOfNestedDictionaries.json"); + id data = [RKTestFixture parsedObjectWithContentsOfFixture:@"ArrayOfNestedDictionaries.json"]; RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:[RKMappableObject class]]; [objectMapping mapKeyPath:@"name" toAttribute:@"stringTest"]; RKObjectMapping *relationshipMapping = [RKObjectMapping mappingForClass:[RKMappableAssociation class]]; [relationshipMapping mapKeyPath:@"title" toAttribute:@"testString"]; [objectMapping mapKeyPath:@"mediaGroups.contents" toRelationship:@"hasMany" withMapping:relationshipMapping]; - RKMappableObject *targetObject = [[RKMappableObject new] autorelease]; + RKMappableObject *targetObject = [[RKMappableObject new] autorelease]; RKLogToComponentWithLevelWhileExecutingBlock(lcl_cRestKitObjectMapping, RKLogLevelDebug, ^ { - RKObjectMappingOperation *operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data + RKObjectMappingOperation *operation = [[RKObjectMappingOperation alloc] initWithSourceObject:data destinationObject:targetObject mapping:objectMapping]; NSError *error = nil; [operation performMapping:&error]; diff --git a/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m b/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m new file mode 100644 index 0000000000..2f7f0fba08 --- /dev/null +++ b/Tests/Logic/ObjectMapping/RKObjectMappingProviderTest.m @@ -0,0 +1,283 @@ +// +// RKObjectMappingProviderTest.m +// RestKit +// +// Created by Greg Combs on 9/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKObjectManager.h" +#import "RKManagedObjectStore.h" +#import "RKTestResponseLoader.h" +#import "RKManagedObjectMapping.h" +#import "RKObjectMappingProvider+Contexts.h" +#import "RKObjectMappingProvider.h" +#import "RKHuman.h" +#import "RKCat.h" +#import "RKObjectMapperTestModel.h" +#import "RKOrderedDictionary.h" + +@interface RKObjectMappingProviderTest : RKTestCase { + RKObjectManager* _objectManager; +} + +@end + +@implementation RKObjectMappingProviderTest + +- (void)setUp { + _objectManager = [RKTestFactory objectManager]; + _objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"RKTests.sqlite"]; + [RKObjectManager setSharedManager:_objectManager]; + [_objectManager.objectStore deletePersistentStore]; +} + +- (void)testShouldFindAnExistingObjectMappingForAClass { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* humanMapping = [RKManagedObjectMapping mappingForClass:[RKHuman class] inManagedObjectStore:objectStore]; + assertThat(humanMapping, isNot(equalTo(nil))); + [humanMapping mapAttributes:@"name", nil]; + [_objectManager.mappingProvider addObjectMapping:humanMapping]; + RKObjectMappingDefinition *returnedMapping = [_objectManager.mappingProvider objectMappingForClass:[RKHuman class]]; + assertThat(returnedMapping, isNot(equalTo(nil))); + assertThat(returnedMapping, is(equalTo(humanMapping))); +} + +- (void)testShouldFindAnExistingObjectMappingForAKeyPath { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; + assertThat(catMapping, isNot(equalTo(nil))); + [catMapping mapAttributes:@"name", nil]; + [_objectManager.mappingProvider setMapping:catMapping forKeyPath:@"cat"]; + RKObjectMappingDefinition *returnedMapping = [_objectManager.mappingProvider mappingForKeyPath:@"cat"]; + assertThat(returnedMapping, isNot(equalTo(nil))); + assertThat(returnedMapping, is(equalTo(catMapping))); +} + +- (void)testShouldAllowYouToRemoveAMappingByKeyPath { + RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKManagedObjectMapping* catMapping = [RKManagedObjectMapping mappingForClass:[RKCat class] inManagedObjectStore:objectStore]; + assertThat(catMapping, isNot(equalTo(nil))); + [catMapping mapAttributes:@"name", nil]; + [mappingProvider setMapping:catMapping forKeyPath:@"cat"]; + RKObjectMappingDefinition *returnedMapping = [mappingProvider mappingForKeyPath:@"cat"]; + assertThat(returnedMapping, isNot(equalTo(nil))); + [mappingProvider removeMappingForKeyPath:@"cat"]; + returnedMapping = [mappingProvider mappingForKeyPath:@"cat"]; + assertThat(returnedMapping, is(nilValue())); +} + +- (void)testSettingMappingInAContext { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + STAssertNoThrow([mappingProvider setMapping:mapping context:1], nil); +} + +- (void)testRetrievalOfMapping { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping context:1]; + assertThat([mappingProvider mappingForContext:1], is(equalTo(mapping))); +} + +- (void)testRetrievalOfMappingsCollectionForUndefinedContextReturnsEmptyArray { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + NSArray *collection = [mappingProvider mappingsForContext:1]; + assertThat(collection, is(empty())); +} + +- (void)testRetrievalOfMappingsCollectionWhenSingleMappingIsStoredRaisesError { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping context:1]; + STAssertThrows([mappingProvider mappingsForContext:1], @"Expected collection mapping retrieval to throw due to storage of single mapping"); +} + +- (void)testAddingMappingToCollectionContext { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + STAssertNoThrow([mappingProvider addMapping:mapping context:1], nil); +} + +- (void)testRetrievalOfMappingCollection { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping_1 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_1 context:1]; + RKObjectMapping *mapping_2 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_2 context:1]; + NSArray *collection = [mappingProvider mappingsForContext:1]; + assertThat(collection, hasItems(mapping_1, mapping_2, nil)); +} + +- (void)testRetrievalOfMappingCollectionReturnsImmutableArray { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping_1 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_1 context:1]; + RKObjectMapping *mapping_2 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_2 context:1]; + NSArray *collection = [mappingProvider mappingsForContext:1]; + assertThat(collection, isNot(instanceOf([NSMutableArray class]))); +} + +- (void)testRemovalOfMappingFromCollection { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping_1 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_1 context:1]; + RKObjectMapping *mapping_2 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider addMapping:mapping_2 context:1]; + [mappingProvider removeMapping:mapping_1 context:1]; + NSArray *collection = [mappingProvider mappingsForContext:1]; + assertThat(collection, onlyContains(mapping_2, nil)); +} + +- (void)testAttemptToRemoveMappingFromContextThatDoesNotIncludeItRaisesError { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + STAssertThrows([mappingProvider removeMapping:mapping context:1], @"Removal of mapping not included in context should raise an error."); +} + +- (void)testSettingMappingForKeyPathInContext { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + STAssertNoThrow([mappingProvider setMapping:mapping forKeyPath:@"testing" context:1], nil); +} + +- (void)testRetrievalOfMappingForKeyPathInContext { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forKeyPath:@"testing" context:1]; + assertThat([mappingProvider mappingForKeyPath:@"testing" context:1], is(equalTo(mapping))); +} + +- (void)testRemovalOfMappingByKeyPathInContext { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forKeyPath:@"testing" context:1]; + [mappingProvider removeMappingForKeyPath:@"testing" context:1]; + assertThat([mappingProvider mappingForKeyPath:@"testing" context:1], is(nilValue())); +} + +- (void)testSettingMappingForPathMatcherCreatesOrderedDictionary { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forPattern:@"/articles/:id" context:1]; + id contextValue = [mappingProvider valueForContext:1]; + assertThat(contextValue, is(instanceOf([RKOrderedDictionary class]))); +} + +- (void)testSettingMappingForPathMatcherCreatesDictionaryWithPathMatcherAsKey { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forPattern:@"/articles/:id" context:1]; + NSDictionary *contextValue = [mappingProvider valueForContext:1]; + assertThat([contextValue allKeys], contains(@"/articles/:id", nil)); +} + +- (void)testSettingMappingForPathMatcherCreatesDictionaryWithMappingAsValue { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + RKObjectMappingProviderContextEntry *entry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; + [mappingProvider setMapping:mapping forPattern:@"/articles/:id" context:1]; + NSDictionary *contextValue = [mappingProvider valueForContext:1]; + assertThat([contextValue allValues], contains(entry, nil)); +} + +- (void)testRetrievalOfMappingForPathMatcher { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forPattern:@"/articles/:id" context:1]; + + RKObjectMappingDefinition * matchedMapping = [mappingProvider mappingForPatternMatchingString:@"/articles/12345" context:1]; + assertThat(matchedMapping, is(equalTo(mapping))); +} + +- (void)testRetrievalOfMappingForPathMatcherIncludingQueryParameters { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping forPattern:@"/articles/:id" context:1]; + + RKObjectMappingDefinition * matchedMapping = [mappingProvider mappingForPatternMatchingString:@"/articles/12345?page=5&this=that" context:1]; + assertThat(matchedMapping, is(equalTo(mapping))); +} + +- (void)testRetrievalOfMappingForPathMatcherWithMultipleEntries { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + + RKObjectMapping *mapping_2 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_2 forPattern:@"/articles/:id\\.json" context:1]; + + RKObjectMapping *mapping_3 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_3 forPattern:@"/articles/:id\\.xml" context:1]; + + RKObjectMapping *mapping_4 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_4 forPattern:@"/articles/:id/comments/:id" context:1]; + + RKObjectMapping *mapping_1 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_1 forPattern:@"/articles/:id" context:1]; + + // Test them + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345" context:1], is(equalTo(mapping_1))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345.json" context:1], is(equalTo(mapping_2))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345.xml" context:1], is(equalTo(mapping_3))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345/comments/3" context:1], is(equalTo(mapping_4))); +} + +- (void)testRetrievalOfMappingForPathMatcherWithEntriesInsertedByIndex { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + + RKObjectMapping *mapping_2 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_2 forPattern:@"/articles/:id\\.json" atIndex:0 context:1]; + + RKObjectMapping *mapping_3 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_3 forPattern:@"/articles/:id\\.xml" atIndex:0 context:1]; + + RKObjectMapping *mapping_4 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_4 forPattern:@"/articles/:id/comments/:id" atIndex:1 context:1]; + + RKObjectMapping *mapping_1 = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + [mappingProvider setMapping:mapping_1 forPattern:@"/articles/:id" atIndex:3 context:1]; + + // Test them + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345" context:1], is(equalTo(mapping_1))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345.json" context:1], is(equalTo(mapping_2))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345.xml" context:1], is(equalTo(mapping_3))); + assertThat([mappingProvider mappingForPatternMatchingString:@"/articles/12345/comments/3" context:1], is(equalTo(mapping_4))); +} + +- (void)testRetrievalOfEntryForPathMatcher { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + RKObjectMappingProviderContextEntry *entry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; + [mappingProvider setEntry:entry forPattern:@"/articles/:id" context:1]; + + RKObjectMappingProviderContextEntry *matchingEntry = [mappingProvider entryForPatternMatchingString:@"/articles/12345" + context:1]; + assertThat(matchingEntry, is(equalTo(entry))); +} + +- (void)testRetrievalOfEntryForPathMatcherIncludingQueryParameters { + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableArray class]]; + RKObjectMappingProviderContextEntry *entry = [RKObjectMappingProviderContextEntry contextEntryWithMapping:mapping]; + [mappingProvider setEntry:entry forPattern:@"/articles/:id" context:1]; + + RKObjectMappingProviderContextEntry *matchingEntry = [mappingProvider entryForPatternMatchingString:@"/articles/12345?page=5&this=that" + context:1]; + assertThat(matchingEntry, is(equalTo(entry))); +} + +@end diff --git a/Specs/ObjectMapping/RKObjectMappingResultSpec.m b/Tests/Logic/ObjectMapping/RKObjectMappingResultTest.m similarity index 88% rename from Specs/ObjectMapping/RKObjectMappingResultSpec.m rename to Tests/Logic/ObjectMapping/RKObjectMappingResultTest.m index 393d4e765b..3c46723788 100644 --- a/Specs/ObjectMapping/RKObjectMappingResultSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectMappingResultTest.m @@ -1,16 +1,16 @@ // -// RKObjectMappingResultSpec.m +// RKObjectMappingResultTest.m // RestKit // // Created by Blake Watters on 7/5/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,14 +18,14 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKObjectMappingResult.h" -@interface RKObjectMappingResultSpec : RKSpec +@interface RKObjectMappingResultTest : RKTestCase @end -@implementation RKObjectMappingResultSpec +@implementation RKObjectMappingResultTest - (void)testShouldNotCrashWhenAsObjectIsInvokedOnAnEmptyResult { NSException* exception = nil; diff --git a/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m b/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m new file mode 100644 index 0000000000..09b0b16e19 --- /dev/null +++ b/Tests/Logic/ObjectMapping/RKObjectPaginatorTest.m @@ -0,0 +1,491 @@ +// +// RKObjectPaginatorTest.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKObjectPaginator.h" +#import "RKObjectMapperTestModel.h" +#import "NSURL+RKAdditions.h" + +NSString * const RKTestPaginatorDelegateTimeoutException = @"RKTestPaginatorDelegateTimeoutException"; + +@interface RKTestPaginatorDelegate : NSObject + +@property (nonatomic, readonly, retain) NSArray *paginatedObjects; +@property (nonatomic, readonly, retain) NSError *paginationError; +@property (nonatomic, readonly) NSUInteger currentPage; +@property (nonatomic, readonly, getter = isLoading) BOOL loading; +@property (nonatomic, assign) NSTimeInterval timeout; + ++ (RKTestPaginatorDelegate *)paginatorDelegate; + +- (BOOL)isLoaded; +- (BOOL)isError; +- (void)waitForLoad; + +@end + +@interface RKTestPaginatorDelegate () +@property (nonatomic, readwrite, retain) NSArray *paginatedObjects; +@property (nonatomic, readwrite, retain) NSError *paginationError; +@end + +@implementation RKTestPaginatorDelegate + +@synthesize paginatedObjects; +@synthesize currentPage; +@synthesize paginationError; +@synthesize loading; +@synthesize timeout; + ++ (RKTestPaginatorDelegate *)paginatorDelegate { + return [[self new] autorelease]; +} + +- (id)init { + self = [super init]; + if (self) { + currentPage = NSIntegerMax; + timeout = 5; + } + + return self; +} + +- (void)dealloc { + [paginatedObjects release]; + [paginationError release]; + + [super dealloc]; +} + +- (BOOL)isLoaded { + return currentPage != NSIntegerMax; +} + +- (BOOL)isError { + return paginationError == nil; +} + +- (void)waitForLoad { + loading = YES; + 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; + } + } +} + +#pragma mark - RKObjectPaginatorDelegate + +- (void)paginator:(RKObjectPaginator *)paginator didLoadObjects:(NSArray *)objects forPage:(NSUInteger)page { + loading = NO; + self.paginatedObjects = objects; + currentPage = page; +} + +- (void)paginator:(RKObjectPaginator *)paginator didFailWithError:(NSError *)error objectLoader:(RKObjectLoader *)loader { + loading = NO; + self.paginationError = error; +} + +- (void)paginator:(RKObjectPaginator *)paginator willLoadPage:(NSUInteger)page objectLoader:(RKObjectLoader *)loader { + // Necessary for OCMock expectations +} + +- (void)paginatorDidLoadFirstPage:(RKObjectPaginator *)paginator { + // Necessary for OCMock expectations +} + +- (void)paginatorDidLoadLastPage:(RKObjectPaginator *)paginator { + // Necessary for OCMock expectations +} + +@end + +@interface RKObjectPaginatorTest : RKTestCase { +} + +@end + +@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"]; + [paginationMapping mapKeyPath:@"per_page" toAttribute:@"perPage"]; + [paginationMapping mapKeyPath:@"total_entries" toAttribute:@"objectCount"]; + + RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKObjectMapperTestModel class]]; + [mapping mapAttributes:@"name", @"age", nil]; + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider mappingProvider]; + mappingProvider.paginationMapping = paginationMapping; + [mappingProvider setObjectMapping:mapping forKeyPath:@"entries"]; + + return mappingProvider; +} + +- (void)testInitCopiesPatternURL { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org"]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + assertThat([paginator.patternURL absoluteString], is(equalTo(@"http://restkit.org"))); +} + +- (void)testInitRetainsMappingProvider { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + assertThat(paginator.mappingProvider, is(equalTo(mappingProvider))); +} + +- (void)testInitDoesNotHavePageCount { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + assertThatBool([paginator hasPageCount], is(equalToBool(NO))); +} + +- (void)testInitDoesNotHaveObjectCount { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + assertThatBool([paginator hasObjectCount], is(equalToBool(NO))); +} + +- (void)testThatLoadWithNilMappingProviderRaisesException { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + NSException *exception = nil; + @try { + [paginator loadPage:1]; + } + @catch (NSException *e) { + exception = e; + } + @finally { + assertThat(exception, is(notNilValue())); + } +} + +- (void)testThatResourcePathPatternEvaluatesAgainstPaginator { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + NSUInteger currentPage = 1; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThat([[paginator URL] resourcePath], is(equalTo(@"/paginate?per_page=25&page=1"))); +} + +- (void)testThatURLReturnsReflectsStateOfPaginator { + RKURL *patternURL = [RKURL URLWithBaseURLString:@"http://restkit.org" resourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:nil]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + NSUInteger currentPage = 1; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + NSDictionary *queryParams = [NSDictionary dictionaryWithObjectsAndKeys: + @"1", @"page", + @"25", @"per_page", nil]; + assertThat([[mockPaginator URL] queryParameters], is(equalTo(queryParams))); +} + +- (void)testLoadingAPageOfObjects { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatBool([testDelegate isLoaded], is(equalToBool(YES))); +} + +- (void)testLoadingPageOfObjectMapsPerPage { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.perPage, is(equalToInteger(3))); +} + +- (void)testLoadingPageOfObjectMapsTotalEntries { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.objectCount, is(equalToInteger(6))); +} + +- (void)testLoadingPageOfObjectMapsCurrentPage { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.currentPage, is(equalToInteger(1))); +} + +- (void)testLoadingPageOfObjectMapsEntriesToObjects { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatInteger([[testDelegate paginatedObjects] count], is(equalToInteger(3))); + assertThat([[testDelegate paginatedObjects] valueForKey:@"name"], is(equalTo([NSArray arrayWithObjects:@"Blake", @"Sarah", @"Colin", nil]))); +} + +- (void)testLoadingPageOfObjectHasPageCount { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatBool([paginator hasPageCount], is(equalToBool(YES))); +} + +- (void)testLoadingPageOfObjectHasObjectCount { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatBool([paginator hasObjectCount], is(equalToBool(YES))); +} + +- (void)testOnDidLoadObjectsForPageBlockIsInvokedOnLoad { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + __block NSArray *blockObjects = nil; + paginator.onDidLoadObjectsForPage = ^(NSArray *objects, NSUInteger page) { + blockObjects = objects; + }; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThat(blockObjects, is(notNilValue())); +} + +- (void)testDelegateIsInformedOfWillLoadPage { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:testDelegate]; + [[mockDelegate expect] paginator:paginator willLoadPage:1 objectLoader:OCMOCK_ANY]; + paginator.delegate = mockDelegate; + [paginator loadPage:1]; + [mockDelegate waitForLoad]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOnError { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:testDelegate]; + [[[mockDelegate expect] andForwardToRealObject] paginator:paginator didFailWithError:OCMOCK_ANY objectLoader:OCMOCK_ANY]; + paginator.delegate = mockDelegate; + [paginator loadPage:999]; + [mockDelegate waitForLoad]; + [mockDelegate verify]; +} + +- (void)testOnDidFailWithErrorBlockIsInvokedOnError { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + __block NSError *expectedError = nil; + paginator.onDidFailWithError = ^(NSError *error, RKObjectLoader *loader) { + expectedError = error; + }; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:999]; + [testDelegate waitForLoad]; + assertThat(expectedError, is(notNilValue())); +} + +- (void)testDelegateIsInformedOnLoadOfFirstPage { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:testDelegate]; + [[mockDelegate expect] paginatorDidLoadFirstPage:paginator]; + paginator.delegate = mockDelegate; + [paginator loadPage:1]; + [mockDelegate waitForLoad]; + [mockDelegate verify]; +} + +- (void)testDelegateIsInformedOnLoadOfLastPage { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + id mockDelegate = [OCMockObject partialMockForObject:testDelegate]; + [[mockDelegate expect] paginatorDidLoadLastPage:paginator]; + paginator.delegate = mockDelegate; + [paginator loadPage:2]; + [mockDelegate waitForLoad]; + [mockDelegate verify]; +} + +- (void)testLoadingNextPageOfObjects { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:1]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.currentPage, is(equalToInteger(1))); + [paginator loadNextPage]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.currentPage, is(equalToInteger(2))); + assertThat([[testDelegate paginatedObjects] valueForKey:@"name"], is(equalTo([NSArray arrayWithObjects:@"Asia", @"Roy", @"Lola", nil]))); +} + +- (void)testLoadingPreviousPageOfObjects { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:2]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.currentPage, is(equalToInteger(2))); + [paginator loadPreviousPage]; + [testDelegate waitForLoad]; + assertThatInteger(paginator.currentPage, is(equalToInteger(1))); + assertThat([[testDelegate paginatedObjects] valueForKey:@"name"], is(equalTo([NSArray arrayWithObjects:@"Blake", @"Sarah", @"Colin", nil]))); +} + +- (void)testFailureWhenLoadingAPageOfObjects { + RKURL *patternURL = [[RKTestFactory baseURL] URLByAppendingResourcePath:RKObjectPaginatorTestResourcePathPattern]; + RKObjectMappingProvider *mappingProvider = [self paginationMappingProvider]; + RKObjectPaginator *paginator = [RKObjectPaginator paginatorWithPatternURL:patternURL mappingProvider:mappingProvider]; + RKTestPaginatorDelegate *testDelegate = [RKTestPaginatorDelegate paginatorDelegate]; + paginator.delegate = testDelegate; + [paginator loadPage:3]; + [testDelegate waitForLoad]; + assertThat(testDelegate.paginationError, is(notNilValue())); +} + +- (void)testKnowledgeOfHasANextPage { + RKObjectPaginator *paginator = [[RKObjectPaginator new] autorelease]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + BOOL isLoaded = YES; + NSUInteger perPage = 5; + NSUInteger pageCount = 3; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(isLoaded)] isLoaded]; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(perPage)] perPage]; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(pageCount)] pageCount]; + + NSUInteger currentPage = 1; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasNextPage], is(equalToBool(YES))); + currentPage = 2; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasNextPage], is(equalToBool(YES))); + currentPage = 3; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasNextPage], is(equalToBool(NO))); +} + +- (void)testHasNextPageRaisesExpectionWhenNotLoaded { + RKObjectPaginator *paginator = [[RKObjectPaginator new] autorelease]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + BOOL loaded = NO; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(loaded)] isLoaded]; + STAssertThrows([mockPaginator hasNextPage], @"Expected exception due to isLoaded == NO"); +} + +- (void)testHasNextPageRaisesExpectionWhenPageCountIsUnknown { + RKObjectPaginator *paginator = [[RKObjectPaginator new] autorelease]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + BOOL loaded = YES; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(loaded)] isLoaded]; + BOOL hasPageCount = NO; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(hasPageCount)] hasPageCount]; + STAssertThrows([mockPaginator hasNextPage], @"Expected exception due to pageCount == NSUIntegerMax"); +} + +- (void)testHasPreviousPageRaisesExpectionWhenNotLoaded { + RKObjectPaginator *paginator = [[RKObjectPaginator new] autorelease]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + BOOL loaded = NO; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(loaded)] isLoaded]; + STAssertThrows([mockPaginator hasPreviousPage], @"Expected exception due to isLoaded == NO"); +} + +- (void)testKnowledgeOfPreviousPage { + RKObjectPaginator *paginator = [[RKObjectPaginator new] autorelease]; + id mockPaginator = [OCMockObject partialMockForObject:paginator]; + BOOL isLoaded = YES; + NSUInteger perPage = 5; + NSUInteger pageCount = 3; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(isLoaded)] isLoaded]; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(perPage)] perPage]; + [[[mockPaginator stub] andReturnValue:OCMOCK_VALUE(pageCount)] pageCount]; + + NSUInteger currentPage = 3; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasPreviousPage], is(equalToBool(YES))); + currentPage = 2; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasPreviousPage], is(equalToBool(YES))); + currentPage = 1; + [[[mockPaginator expect] andReturnValue:OCMOCK_VALUE(currentPage)] currentPage]; + assertThatBool([mockPaginator hasPreviousPage], is(equalToBool(NO))); +} + +@end diff --git a/Specs/ObjectMapping/RKObjectRouterSpec.m b/Tests/Logic/ObjectMapping/RKObjectRouterTest.m similarity index 51% rename from Specs/ObjectMapping/RKObjectRouterSpec.m rename to Tests/Logic/ObjectMapping/RKObjectRouterTest.m index b9981d53bd..328e164588 100644 --- a/Specs/ObjectMapping/RKObjectRouterSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectRouterTest.m @@ -1,16 +1,16 @@ // -// RKObjectRouterSpec.m +// RKObjectRouterTest.m // RestKit // // Created by Blake Watters on 7/20/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,38 +18,44 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "NSManagedObject+ActiveRecord.h" #import "RKManagedObjectStore.h" -#import "RKHuman.h" -#import "RKCat.h" +#import "RKTestUser.h" -@interface RKSpecObject : NSObject +@interface RKTestObject : NSObject @end -@implementation RKSpecObject +@implementation RKTestObject ++ (id)object { + return [[self new] autorelease]; +} @end -@interface RKSpecSubclassedObject : RKSpecObject +@interface RKTestSubclassedObject : RKTestObject @end -@implementation RKSpecSubclassedObject +@implementation RKTestSubclassedObject @end -@interface RKObjectRouterSpec : RKSpec { +@interface RKObjectRouterTest : RKTestCase { } @end -@implementation RKObjectRouterSpec +@implementation RKTestUser (PolymorphicResourcePath) -- (void)setUp { - RKSpecNewManagedObjectStore(); +- (NSString *)polymorphicResourcePath { + return @"/this/is/the/path"; } --(void)itShouldThrowAnExceptionWhenAskedForAPathForAnUnregisteredClassAndMethod { +@end + +@implementation RKObjectRouterTest + +-(void)testThrowAnExceptionWhenAskedForAPathForAnUnregisteredClassAndMethod { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; NSException* exception = nil; @try { - [router resourcePathForObject:[RKHuman object] method:RKRequestMethodPOST]; + [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodPOST]; } @catch (NSException * e) { exception = e; @@ -57,12 +63,12 @@ -(void)itShouldThrowAnExceptionWhenAskedForAPathForAnUnregisteredClassAndMethod assertThat(exception, isNot(nilValue())); } --(void)itShouldThrowAnExceptionWhenAskedForAPathForARegisteredClassButUnregisteredMethod { +-(void)testThrowAnExceptionWhenAskedForAPathForARegisteredClassButUnregisteredMethod { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; NSException* exception = nil; @try { - [router resourcePathForObject:[RKHuman object] method:RKRequestMethodPOST]; + [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodPOST]; } @catch (NSException * e) { exception = e; @@ -70,57 +76,57 @@ -(void)itShouldThrowAnExceptionWhenAskedForAPathForARegisteredClassButUnregister assertThat(exception, isNot(nilValue())); } --(void)itShouldReturnPathsRegisteredForSpecificRequestMethods { +-(void)testReturnPathsRegisteredForTestificRequestMethods { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; - NSString* path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; + NSString* path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodGET]; assertThat(path, is(equalTo(@"/HumanService.asp"))); } --(void)itShouldReturnPathsRegisteredForTheClassAsAWhole { +-(void)testReturnPathsRegisteredForTheClassAsAWhole { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp"]; - NSString* path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp"]; + NSString* path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodGET]; assertThat(path, is(equalTo(@"/HumanService.asp"))); - path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodPOST]; + path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodPOST]; assertThat(path, is(equalTo(@"/HumanService.asp"))); } - (void)testShouldReturnPathsIfTheSuperclassIsRegistered { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKSpecObject class] toResourcePath:@"/HumanService.asp"]; - NSString* path = [router resourcePathForObject:[RKSpecSubclassedObject new] method:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp"]; + NSString* path = [router resourcePathForObject:[RKTestSubclassedObject new] method:RKRequestMethodGET]; assertThat(path, is(equalTo(@"/HumanService.asp"))); } - (void)testShouldFavorExactMatcherOverSuperclassMatches { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKSpecObject class] toResourcePath:@"/HumanService.asp"]; - [router routeClass:[RKSpecSubclassedObject class] toResourcePath:@"/SubclassedHumanService.asp"]; - NSString* path = [router resourcePathForObject:[RKSpecSubclassedObject new] method:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp"]; + [router routeClass:[RKTestSubclassedObject class] toResourcePath:@"/SubclassedHumanService.asp"]; + NSString* path = [router resourcePathForObject:[RKTestSubclassedObject new] method:RKRequestMethodGET]; assertThat(path, is(equalTo(@"/SubclassedHumanService.asp"))); - path = [router resourcePathForObject:[RKSpecObject new] method:RKRequestMethodPOST]; + path = [router resourcePathForObject:[RKTestObject new] method:RKRequestMethodPOST]; assertThat(path, is(equalTo(@"/HumanService.asp"))); } --(void)itShouldFavorSpecificMethodsWhenClassAndSpecificMethodsAreRegistered { +-(void)testFavorTestificMethodsWhenClassAndTestificMethodsAreRegistered { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp"]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanServiceForPUT.asp" forMethod:RKRequestMethodPUT]; - NSString* path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp"]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanServiceForPUT.asp" forMethod:RKRequestMethodPUT]; + NSString* path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodGET]; assertThat(path, is(equalTo(@"/HumanService.asp"))); - path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodPOST]; + path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodPOST]; assertThat(path, is(equalTo(@"/HumanService.asp"))); - path = [router resourcePathForObject:[RKHuman object] method:RKRequestMethodPUT]; + path = [router resourcePathForObject:[RKTestObject object] method:RKRequestMethodPUT]; assertThat(path, is(equalTo(@"/HumanServiceForPUT.asp"))); } --(void)itShouldRaiseAnExceptionWhenAttemptIsMadeToRegisterOverAnExistingRoute { +-(void)testRaiseAnExceptionWhenAttemptIsMadeToRegisterOverAnExistingRoute { RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; NSException* exception = nil; @try { - [router routeClass:[RKHuman class] toResourcePath:@"/HumanService.asp" forMethod:RKRequestMethodGET]; + [router routeClass:[RKTestObject class] toResourcePathPattern:@"/HumanService.asp" forMethod:RKRequestMethodGET]; } @catch (NSException * e) { exception = e; @@ -129,45 +135,45 @@ -(void)itShouldRaiseAnExceptionWhenAttemptIsMadeToRegisterOverAnExistingRoute { } - (void)testShouldInterpolatePropertyNamesReferencedInTheMapping { - RKHuman* blake = [RKHuman object]; + RKTestUser* blake = [RKTestUser user]; blake.name = @"blake"; - blake.railsID = [NSNumber numberWithInt:31337]; + blake.userID = [NSNumber numberWithInt:31337]; RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/humans/:railsID/:name" forMethod:RKRequestMethodGET]; - + [router routeClass:[RKTestUser class] toResourcePathPattern:@"/humans/:userID/:name" forMethod:RKRequestMethodGET]; + NSString* resourcePath = [router resourcePathForObject:blake method:RKRequestMethodGET]; assertThat(resourcePath, is(equalTo(@"/humans/31337/blake"))); } - (void)testShouldInterpolatePropertyNamesReferencedInTheMappingWithDeprecatedParentheses { - RKHuman* blake = [RKHuman object]; + RKTestUser* blake = [RKTestUser user]; blake.name = @"blake"; - blake.railsID = [NSNumber numberWithInt:31337]; + blake.userID = [NSNumber numberWithInt:31337]; RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"/humans/(railsID)/(name)" forMethod:RKRequestMethodGET]; - + [router routeClass:[RKTestUser class] toResourcePathPattern:@"/humans/(userID)/(name)" forMethod:RKRequestMethodGET]; + NSString* resourcePath = [router resourcePathForObject:blake method:RKRequestMethodGET]; assertThat(resourcePath, is(equalTo(@"/humans/31337/blake"))); } - (void)testShouldAllowForPolymorphicURLsViaMethodCalls { - RKHuman* blake = [RKHuman object]; + RKTestUser* blake = [RKTestUser user]; blake.name = @"blake"; - blake.railsID = [NSNumber numberWithInt:31337]; + blake.userID = [NSNumber numberWithInt:31337]; RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@":polymorphicResourcePath" forMethod:RKRequestMethodGET escapeRoutedPath:NO]; - + [router routeClass:[RKTestUser class] toResourcePathPattern:@":polymorphicResourcePath" forMethod:RKRequestMethodGET escapeRoutedPath:NO]; + NSString* resourcePath = [router resourcePathForObject:blake method:RKRequestMethodGET]; assertThat(resourcePath, is(equalTo(@"/this/is/the/path"))); } - (void)testShouldAllowForPolymorphicURLsViaMethodCallsWithDeprecatedParentheses { - RKHuman* blake = [RKHuman object]; + RKTestUser* blake = [RKTestUser user]; blake.name = @"blake"; - blake.railsID = [NSNumber numberWithInt:31337]; + blake.userID = [NSNumber numberWithInt:31337]; RKObjectRouter* router = [[[RKObjectRouter alloc] init] autorelease]; - [router routeClass:[RKHuman class] toResourcePath:@"(polymorphicResourcePath)" forMethod:RKRequestMethodGET escapeRoutedPath:NO]; - + [router routeClass:[RKTestUser class] toResourcePathPattern:@"(polymorphicResourcePath)" forMethod:RKRequestMethodGET escapeRoutedPath:NO]; + NSString* resourcePath = [router resourcePathForObject:blake method:RKRequestMethodGET]; assertThat(resourcePath, is(equalTo(@"/this/is/the/path"))); } diff --git a/Specs/ObjectMapping/RKObjectSerializerSpec.m b/Tests/Logic/ObjectMapping/RKObjectSerializerTest.m similarity index 90% rename from Specs/ObjectMapping/RKObjectSerializerSpec.m rename to Tests/Logic/ObjectMapping/RKObjectSerializerTest.m index 31f864ce8d..fc2bc65e7a 100644 --- a/Specs/ObjectMapping/RKObjectSerializerSpec.m +++ b/Tests/Logic/ObjectMapping/RKObjectSerializerTest.m @@ -1,16 +1,16 @@ // -// RKObjectSerializerSpec.m +// RKObjectSerializerTest.m // RestKit // // Created by Jeremy Ellison on 5/9/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,16 +18,16 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKObjectSerializer.h" #import "RKMappableObject.h" -@interface RKObjectSerializerSpec : RKSpec { +@interface RKObjectSerializerTest : RKTestCase { } @end -@implementation RKObjectSerializerSpec +@implementation RKObjectSerializerTest - (void)testShouldSerializeToFormEncodedData { NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2", nil]; @@ -51,10 +51,10 @@ - (void)testShouldSerializeADateToFormEncodedData { RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/x-www-form-urlencoded" error:&error]; - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"key1-form-name=value1&date-form-name=1970-01-01 00:00:00 +0000"))); } @@ -71,10 +71,10 @@ - (void)testShouldSerializeADateToAStringUsingThePreferredDateFormatter { RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/x-www-form-urlencoded" error:&error]; - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"key1-form-name=value1&date-form-name=01/01/1970"))); } @@ -85,13 +85,13 @@ - (void)testShouldSerializeADateToJSON { [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"key1" toKeyPath:@"key1-form-name"]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"date" toKeyPath:@"date-form-name"]]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; - + NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"{\"key1-form-name\":\"value1\",\"date-form-name\":\"1970-01-01 00:00:00 +0000\"}"))); } @@ -102,13 +102,13 @@ - (void)testShouldSerializeNSDecimalNumberAttributesToJSON { [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"key1" toKeyPath:@"key1-form-name"]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"number" toKeyPath:@"number-form-name"]]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; - + NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"{\"key1-form-name\":\"value1\",\"number-form-name\":\"18274191731731.4557723623\"}"))); } @@ -120,19 +120,19 @@ - (void)testShouldSerializeRelationshipsToo { [NSDictionary dictionaryWithObjectsAndKeys:@"relationship1Value2", @"relatioship1Key1", nil], nil], @"relationship1", [NSDictionary dictionaryWithObjectsAndKeys:@"subValue1", @"subKey1", nil], @"relationship2", nil]; - + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"key1" toKeyPath:@"key1-form-name"]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"key2" toKeyPath:@"key2-form-name"]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"relationship1.relatioship1Key1" toKeyPath:@"relationship1-form-name[r1k1]"]]; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"relationship2.subKey1" toKeyPath:@"relationship2-form-name[subKey1]"]]; - + RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/x-www-form-urlencoded" error:&error]; NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); #if TARGET_OS_IPHONE assertThat(data, is(equalTo(@"key1-form-name=value1&relationship1-form-name[r1k1][]=relationship1Value1&relationship1-form-name[r1k1][]=relationship1Value2&key2-form-name=value2&relationship2-form-name[subKey1]=subValue1"))); @@ -151,7 +151,7 @@ - (void)testShouldSerializeToJSON { id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"{\"key2-form-name\":\"value2\",\"key1-form-name\":\"value1\"}"))); } @@ -163,7 +163,7 @@ - (void)testShouldSetReturnNilIfItDoesNotFindAnythingToSerialize { RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; - + assertThat(serialization, is(nilValue())); } @@ -173,23 +173,23 @@ - (void)testShouldSerializeNestedObjectsContainingDatesToJSON { RKMappableAssociation* association = [[RKMappableAssociation new] autorelease]; association.date = [NSDate dateWithTimeIntervalSince1970:0]; object.hasOne = association; - + // Setup object mappings RKObjectMapping* objectMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; - [objectMapping mapAttributes:@"stringTest", nil]; + [objectMapping mapAttributes:@"stringTest", nil]; RKObjectMapping* relationshipMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; [relationshipMapping mapAttributes:@"date", nil]; [objectMapping mapRelationship:@"hasOne" withMapping:relationshipMapping]; - + // Serialize RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:objectMapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; assertThat(error, is(nilValue())); - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + // Encodes differently on iOS / OS X #if TARGET_OS_IPHONE assertThat(data, is(equalTo(@"{\"stringTest\":\"The string\",\"hasOne\":{\"date\":\"1970-01-01 00:00:00 +0000\"}}"))); @@ -209,7 +209,7 @@ - (void)testShouldEncloseTheSerializationInAContainerIfRequested { id serialization = [serializer serializationForMIMEType:@"application/x-www-form-urlencoded" error:&error]; NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"stuff[key2-form-name]=value2&stuff[key1-form-name]=value1"))); } @@ -220,20 +220,20 @@ - (void)testShouldSerializeToManyRelationships { RKMappableAssociation* association = [[RKMappableAssociation new] autorelease]; association.date = [NSDate dateWithTimeIntervalSince1970:0]; object.hasMany = [NSSet setWithObject:association]; - + // Setup object mappings RKObjectMapping* objectMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; - [objectMapping mapAttributes:@"stringTest", nil]; + [objectMapping mapAttributes:@"stringTest", nil]; RKObjectMapping* relationshipMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]]; [relationshipMapping mapAttributes:@"date", nil]; [objectMapping mapRelationship:@"hasMany" withMapping:relationshipMapping]; - + // Serialize RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:objectMapping]; NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; assertThat(error, is(nilValue())); - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; assertThat(data, is(equalTo(@"{\"hasMany\":[{\"date\":\"1970-01-01 00:00:00 +0000\"}],\"stringTest\":\"The string\"}"))); @@ -245,15 +245,34 @@ - (void)testShouldSerializeAnNSNumberContainingABooleanToTrueFalseIfRequested { RKObjectAttributeMapping* attributeMapping = [RKObjectAttributeMapping mappingFromKeyPath:@"boolean" toKeyPath:@"boolean-value"]; [mapping addAttributeMapping:attributeMapping]; RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; - + NSError* error = nil; id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; - + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - + assertThat(error, is(nilValue())); assertThat(data, is(equalTo(@"{\"boolean-value\":true}"))); } +- (void)testShouldSerializeANSOrderedSetToJSON { + NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", + [NSOrderedSet orderedSetWithObjects:@"setElementOne", @"setElementTwo", @"setElementThree", nil], @"set", + nil]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[NSDictionary class]]; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"key1" toKeyPath:@"key1-form-name"]]; + [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"set" toKeyPath:@"set-form-name"]]; + RKObjectSerializer* serializer = [RKObjectSerializer serializerWithObject:object mapping:mapping]; + + NSError* error = nil; + id serialization = [serializer serializationForMIMEType:@"application/json" error:&error]; + + NSString* data = [[[NSString alloc] initWithData:[serialization HTTPBody] encoding:NSUTF8StringEncoding] autorelease]; + data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + assertThat(error, is(nilValue())); + assertThat(data, is(equalTo(@"{\"key1-form-name\":\"value1\",\"set-form-name\":[\"setElementOne\",\"setElementTwo\",\"setElementThree\"]}"))); +} + @end diff --git a/Tests/Logic/ObjectMapping/RKParserRegistryTest.m b/Tests/Logic/ObjectMapping/RKParserRegistryTest.m new file mode 100644 index 0000000000..0304510c93 --- /dev/null +++ b/Tests/Logic/ObjectMapping/RKParserRegistryTest.m @@ -0,0 +1,88 @@ +// +// RKParserRegistryTest.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKTestEnvironment.h" +#import "RKParserRegistry.h" +#import "RKJSONParserJSONKit.h" +#import "RKXMLParserXMLReader.h" + +@interface RKParserRegistryTest : RKTestCase { +} + +@end + +@implementation RKParserRegistryTest + +- (void)testShouldEnableRegistrationFromMIMETypeToParserClasses { + RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; + [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; + Class parserClass = [registry parserClassForMIMEType:RKMIMETypeJSON]; + assertThat(NSStringFromClass(parserClass), is(equalTo(@"RKJSONParserJSONKit"))); +} + +- (void)testShouldInstantiateParserObjects { + RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; + [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; + id parser = [registry parserForMIMEType:RKMIMETypeJSON]; + assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); +} + +- (void)testShouldAutoconfigureBasedOnReflection { + RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; + [registry autoconfigure]; + id parser = [registry parserForMIMEType:RKMIMETypeJSON]; + assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); + parser = [registry parserForMIMEType:RKMIMETypeXML]; + assertThat(parser, is(instanceOf([RKXMLParserXMLReader class]))); +} + +- (void)testRetrievalOfExactStringMatchForMIMEType { + RKParserRegistry* registry = [[RKParserRegistry new] autorelease]; + [registry setParserClass:[RKJSONParserJSONKit class] forMIMEType:RKMIMETypeJSON]; + id parser = [registry parserForMIMEType:RKMIMETypeJSON]; + assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); +} + +- (void)testRetrievalOfRegularExpressionMatchForMIMEType { + 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]; + id parser = [registry parserForMIMEType:@"application/xml+whatever"]; + assertThat(parser, is(instanceOf([RKJSONParserJSONKit class]))); +} + +- (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:[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]))); +} + +@end 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/Specs/Support/NSDictionary+RKRequestSerializationSpec.m b/Tests/Logic/Support/NSDictionary+RKRequestSerializationTest.m similarity index 93% rename from Specs/Support/NSDictionary+RKRequestSerializationSpec.m rename to Tests/Logic/Support/NSDictionary+RKRequestSerializationTest.m index 86a7ba668f..e0392e2d5a 100644 --- a/Specs/Support/NSDictionary+RKRequestSerializationSpec.m +++ b/Tests/Logic/Support/NSDictionary+RKRequestSerializationTest.m @@ -1,16 +1,16 @@ // -// NSDictionary+RKRequestSerializationSpec.m +// NSDictionary+RKRequestSerializationTest.m // RestKit // // Created by Blake Watters on 2/24/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,16 +18,16 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "NSDictionary+RKRequestSerialization.h" #import "NSDictionary+RKAdditions.h" -@interface NSDictionary_RKRequestSerializationSpec : RKSpec { +@interface NSDictionary_RKRequestSerializationTest : RKTestCase { } @end -@implementation NSDictionary_RKRequestSerializationSpec +@implementation NSDictionary_RKRequestSerializationTest - (void)testShouldHaveKeysAndValuesDictionaryInitializer { NSDictionary* dictionary1 = [NSDictionary dictionaryWithObjectsAndKeys:@"value", @"key", @"value2", @"key2", nil]; diff --git a/Specs/Support/NSStringRestKitSpec.m b/Tests/Logic/Support/NSStringRestKitTest.m similarity index 78% rename from Specs/Support/NSStringRestKitSpec.m rename to Tests/Logic/Support/NSStringRestKitTest.m index 16378a70f2..c9ddcf2d9b 100755 --- a/Specs/Support/NSStringRestKitSpec.m +++ b/Tests/Logic/Support/NSStringRestKitTest.m @@ -1,16 +1,16 @@ // -// NSStringRestKitSpec.m +// NSStringRestKitTest.m // RestKit // // Created by Greg Combs on 9/2/11. -// Copyright 2011 RestKit -// +// 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. @@ -18,32 +18,32 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" -#import "NSString+RestKit.h" -#import "RKObjectMapperSpecModel.h" +#import "RKTestEnvironment.h" +#import "NSString+RKAdditions.h" +#import "RKObjectMapperTestModel.h" -@interface NSStringRestKitSpec : RKSpec +@interface NSStringRestKitTest : RKTestCase @end -@implementation NSStringRestKitSpec +@implementation NSStringRestKitTest - (void)testShouldAppendQueryParameters { NSString *resourcePath = @"/controller/objects/"; NSDictionary *queryParams = [NSDictionary dictionaryWithObjectsAndKeys: @"ascend", @"sortOrder", @"name", @"groupBy",nil]; - NSString *resultingPath = [resourcePath appendQueryParams:queryParams]; + NSString *resultingPath = [resourcePath stringByAppendingQueryParameters:queryParams]; assertThat(resultingPath, isNot(equalTo(nil))); NSString *expectedPath1 = @"/controller/objects/?sortOrder=ascend&groupBy=name"; NSString *expectedPath2 = @"/controller/objects/?groupBy=name&sortOrder=ascend"; - BOOL isValidPath = ( [resultingPath isEqualToString:expectedPath1] || + BOOL isValidPath = ( [resultingPath isEqualToString:expectedPath1] || [resultingPath isEqualToString:expectedPath2] ); assertThatBool(isValidPath, is(equalToBool(YES))); } - (void)testShouldInterpolateObjects { - RKObjectMapperSpecModel *person = [[[RKObjectMapperSpecModel alloc] init] autorelease]; + RKObjectMapperTestModel *person = [[[RKObjectMapperTestModel alloc] init] autorelease]; person.name = @"CuddleGuts"; person.age = [NSNumber numberWithInt:6]; NSString *interpolatedPath = [@"/people/:name/:age" interpolateWithObject:person]; @@ -53,7 +53,7 @@ - (void)testShouldInterpolateObjects { } - (void)testShouldInterpolateObjectsWithDeprecatedParentheses { - RKObjectMapperSpecModel *person = [[[RKObjectMapperSpecModel alloc] init] autorelease]; + RKObjectMapperTestModel *person = [[[RKObjectMapperTestModel alloc] init] autorelease]; person.name = @"CuddleGuts"; person.age = [NSNumber numberWithInt:6]; NSString *interpolatedPath = [@"/people/(name)/(age)" interpolateWithObject:person]; @@ -70,12 +70,17 @@ - (void)testShouldParseQueryParameters { assertThat(queryParams, hasEntries(@"keyA", @"valA", @"keyB", @"valB", nil)); } -- (void)testShouldReturnTheMIMETypeForAPath { +- (void)testReturningTheMIMETypeForAPathWithXMLExtension { NSString *MIMEType = [@"/path/to/file.xml" MIMETypeForPathExtension]; assertThat(MIMEType, is(equalTo(@"application/xml"))); } -- (void)itShouldKnowIfTheReceiverContainsAnIPAddress { +- (void)testReturningTheMIMETypeForAPathWithJSONExtension { + NSString *MIMEType = [@"/path/to/file.json" MIMETypeForPathExtension]; + assertThat(MIMEType, is(equalTo(@"application/json"))); +} + +- (void)testShouldKnowIfTheReceiverContainsAnIPAddress { assertThatBool([@"127.0.0.1" isIPAddress], equalToBool(YES)); assertThatBool([@"173.45.234.197" isIPAddress], equalToBool(YES)); assertThatBool([@"google.com" isIPAddress], equalToBool(NO)); 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/Specs/Support/RKDotNetDateFormatterSpec.m b/Tests/Logic/Support/RKDotNetDateFormatterTest.m similarity index 80% rename from Specs/Support/RKDotNetDateFormatterSpec.m rename to Tests/Logic/Support/RKDotNetDateFormatterTest.m index adf211b509..de14dbf9a2 100644 --- a/Specs/Support/RKDotNetDateFormatterSpec.m +++ b/Tests/Logic/Support/RKDotNetDateFormatterTest.m @@ -1,30 +1,30 @@ // -// RKDotNetDateFormatterSpec.m +// RKDotNetDateFormatterTest.m // RestKit // // Created by Greg Combs on 9/8/11. -// Copyright (c) 2011 RestKit. All rights reserved. +// Copyright (c) 2009-2012 RestKit. All rights reserved. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKDotNetDateFormatter.h" -@interface RKDotNetDateFormatterSpec : RKSpec +@interface RKDotNetDateFormatterTest : RKTestCase @end -@implementation RKDotNetDateFormatterSpec +@implementation RKDotNetDateFormatterTest - (void)testShouldInstantiateAFormatterWithDefaultGMTTimeZone { RKDotNetDateFormatter *formatter = [RKDotNetDateFormatter dotNetDateFormatter]; - NSTimeZone *timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + NSTimeZone *timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; assertThat(formatter, isNot(equalTo(nil))); assertThat(formatter.timeZone, is(equalTo(timeZone))); } - (void)testShouldInstantiateAFormatterWithATimeZone { - NSTimeZone *timeZoneCST = [NSTimeZone timeZoneWithAbbreviation:@"CST"]; + NSTimeZone *timeZoneCST = [NSTimeZone timeZoneWithAbbreviation:@"CST"]; RKDotNetDateFormatter *formatter = [RKDotNetDateFormatter dotNetDateFormatterWithTimeZone:timeZoneCST]; assertThat(formatter, isNot(equalTo(nil))); assertThat(formatter.timeZone, is(equalTo(timeZoneCST))); @@ -62,7 +62,7 @@ - (void)testShouldFailToCreateADateFromInvalidStrings { } - (void)testShouldCreateADotNetStringFromADateWithATimeZone { - NSTimeZone *timeZoneEST = [NSTimeZone timeZoneWithAbbreviation:@"EST"]; + NSTimeZone *timeZoneEST = [NSTimeZone timeZoneWithAbbreviation:@"EST"]; RKDotNetDateFormatter *formatter = [RKDotNetDateFormatter dotNetDateFormatterWithTimeZone:timeZoneEST]; NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:1000212360]; NSString *string = [formatter stringFromDate:referenceDate]; @@ -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/Specs/Support/RKJSONParserJSONKitSpec.m b/Tests/Logic/Support/RKJSONParserJSONKitTest.m similarity index 81% rename from Specs/Support/RKJSONParserJSONKitSpec.m rename to Tests/Logic/Support/RKJSONParserJSONKitTest.m index f8cd9cdc69..4a5637e745 100644 --- a/Specs/Support/RKJSONParserJSONKitSpec.m +++ b/Tests/Logic/Support/RKJSONParserJSONKitTest.m @@ -1,16 +1,16 @@ // -// RKJSONParserJSONKitSpec.m +// RKJSONParserJSONKitTest.m // RestKit // // Created by Blake Watters on 7/6/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,14 +18,14 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKJSONParserJSONKit.h" -@interface RKJSONParserJSONKitSpec : RKSpec +@interface RKJSONParserJSONKitTest : RKTestCase @end -@implementation RKJSONParserJSONKitSpec +@implementation RKJSONParserJSONKitTest - (void)testShouldParseEmptyResults { NSError* error = nil; diff --git a/Tests/Logic/Support/RKMutableBlockDictionaryTest.m b/Tests/Logic/Support/RKMutableBlockDictionaryTest.m new file mode 100644 index 0000000000..544ffbaef9 --- /dev/null +++ b/Tests/Logic/Support/RKMutableBlockDictionaryTest.m @@ -0,0 +1,37 @@ +// +// RKMutableBlockDictionaryTest.m +// RestKit +// +// Created by Blake Watters on 8/22/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "RKMutableBlockDictionary.h" + +@interface RKMutableBlockDictionaryTest : RKTestCase + +@end + +@implementation RKMutableBlockDictionaryTest + +- (void)testLetYouAssignABlockToTheDictionary { + RKMutableBlockDictionary* blockDictionary = [[RKMutableBlockDictionary new] autorelease]; + [blockDictionary setValueWithBlock:^id{ return @"Value from the block!"; } forKey:@"theKey"]; + assertThat([blockDictionary valueForKey:@"theKey"], is(equalTo(@"Value from the block!"))); +} + +- (void)testLetYouUseKVC { + RKMutableBlockDictionary* blockDictionary = [[RKMutableBlockDictionary new] autorelease]; + [blockDictionary setValue:@"a value" forKey:@"a key"]; + assertThat([blockDictionary valueForKey:@"a key"], is(equalTo(@"a value"))); +} + +- (void)testLetYouAccessABlockValueUsingAKeyPath { + RKMutableBlockDictionary* blockDictionary = [[RKMutableBlockDictionary new] autorelease]; + [blockDictionary setValueWithBlock:^id{ return @"Value from the block!"; } forKey:@"theKey"]; + NSDictionary* otherDictionary = [NSDictionary dictionaryWithObject:blockDictionary forKey:@"dictionary"]; + assertThat([otherDictionary valueForKeyPath:@"dictionary.theKey"], is(equalTo(@"Value from the block!"))); +} + +@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/Specs/Support/RKPathMatcherSpec.m b/Tests/Logic/Support/RKPathMatcherTest.m similarity index 96% rename from Specs/Support/RKPathMatcherSpec.m rename to Tests/Logic/Support/RKPathMatcherTest.m index 7d8075a39e..c4124effb8 100755 --- a/Specs/Support/RKPathMatcherSpec.m +++ b/Tests/Logic/Support/RKPathMatcherTest.m @@ -1,16 +1,16 @@ // -// RKPathMatcherSpec.m +// RKPathMatcherTest.m // RestKit // // Created by Greg Combs on 9/2/11. -// Copyright 2011 RestKit -// +// 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. @@ -18,14 +18,14 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" +#import "RKTestEnvironment.h" #import "RKPathMatcher.h" -@interface RKPathMatcherSpec : RKSpec +@interface RKPathMatcherTest : RKTestCase @end -@implementation RKPathMatcherSpec +@implementation RKPathMatcherTest - (void)testShouldMatchPathsWithQueryArguments { NSDictionary *arguments = nil; @@ -34,7 +34,7 @@ - (void)testShouldMatchPathsWithQueryArguments { assertThatBool(isMatchingPattern, is(equalToBool(YES))); assertThat(arguments, isNot(empty())); assertThat(arguments, hasEntries(@"controllerName", @"my", @"entityName", @"backend", @"foo", @"bar", @"this", @"that", nil)); - + } - (void)testShouldMatchPathsWithEscapedArguments { @@ -44,7 +44,7 @@ - (void)testShouldMatchPathsWithEscapedArguments { assertThatBool(isMatchingPattern, is(equalToBool(YES))); assertThat(arguments, isNot(empty())); assertThat(arguments, hasEntries(@"stateID", @"tx", @"session", @"82", @"billID", @"SB 14", @"apikey", @"GC12d0c6af", nil)); - + } - (void)testShouldMatchPathsWithoutQueryArguments { @@ -69,7 +69,7 @@ - (void)testShouldPerformTwoMatchesInARow { RKPathMatcher *pathMatcher = [RKPathMatcher matcherWithPath:@"/metadata?apikey=GC12d0c6af"]; BOOL isMatchingPattern1 = [pathMatcher matchesPattern:@"/metadata/:stateID" tokenizeQueryStrings:YES parsedArguments:&arguments]; assertThatBool(isMatchingPattern1, is(equalToBool(NO))); - BOOL isMatchingPattern2 = [pathMatcher matchesPattern:@"/metadata" tokenizeQueryStrings:YES parsedArguments:&arguments]; + BOOL isMatchingPattern2 = [pathMatcher matchesPattern:@"/metadata" tokenizeQueryStrings:YES parsedArguments:&arguments]; assertThatBool(isMatchingPattern2, is(equalToBool(YES))); assertThat(arguments, isNot(empty())); assertThat(arguments, hasEntry(@"apikey", @"GC12d0c6af")); diff --git a/Specs/Support/RKXMLParserSpec.m b/Tests/Logic/Support/RKXMLParserTest.m similarity index 52% rename from Specs/Support/RKXMLParserSpec.m rename to Tests/Logic/Support/RKXMLParserTest.m index b2574ec55d..d7a5b5e1ee 100644 --- a/Specs/Support/RKXMLParserSpec.m +++ b/Tests/Logic/Support/RKXMLParserTest.m @@ -1,16 +1,16 @@ // -// RKXMLParserLibXMLSpec.m +// RKXMLParserTest.m // RestKit // // Created by Jeremy Ellison on 3/29/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,11 +18,11 @@ // limitations under the License. // -#import "RKSpecEnvironment.h" -#import "RKXMLParserLibXML.h" +#import "RKTestEnvironment.h" +#import "RKXMLParserXMLReader.h" -// See Specs/Fixtures/XML/tab_data.xml -@interface RKSpecTabData : NSObject { +// See Tests/Fixtures/XML/tab_data.xml +@interface RKTestTabData : NSObject { NSString* _title; NSString* _summary; } @@ -32,51 +32,53 @@ @interface RKSpecTabData : NSObject { @end -@implementation RKSpecTabData +@implementation RKTestTabData @synthesize title = _title; @synthesize summary = _summary; @end -@interface RKXMLParserLibXMLSpec : RKSpec { - +@interface RKXMLParserTest : RKTestCase { + } @end -@implementation RKXMLParserLibXMLSpec +@implementation RKXMLParserTest - (void)testShouldMapASingleXMLObjectPayloadToADictionary { NSString* data = @"\n\n 2.4\n string\n 1\n\n"; - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - id result = [parser parseXML:data]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + id result = [parser objectFromString:data error:&error]; assertThat(NSStringFromClass([result class]), is(equalTo(@"__NSCFDictionary"))); - assertThatFloat([[result valueForKeyPath:@"hash.float"] floatValue], is(equalToFloat(2.4f))); - assertThatInt([[result valueForKeyPath:@"hash.number"] intValue], is(equalToInt(1))); + assertThatFloat([[[result valueForKeyPath:@"hash.float"] valueForKey:@"text"] floatValue], is(equalToFloat(2.4f))); + assertThatInt([[[result valueForKeyPath:@"hash.number"] valueForKey:@"text"] intValue], is(equalToInt(1))); assertThat([result valueForKeyPath:@"hash.string"], is(equalTo(@"string"))); } - (void)testShouldMapMultipleObjectsToAnArray { NSString* data = @"\n\n \n 2.4\n string\n 1\n \n \n 1\n \n\n"; - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - id result = [parser parseXML:data]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + id result = [parser objectFromString:data error:&error]; NSArray* records = (NSArray*)[result valueForKeyPath:@"records.record"]; assertThatUnsignedInteger([records count], is(equalToInt(2))); id result1 = [records objectAtIndex:0]; assertThat(NSStringFromClass([result1 class]), is(equalTo(@"__NSCFDictionary"))); - assertThatFloat([[result1 valueForKeyPath:@"float"] floatValue], is(equalToFloat(2.4f))); - assertThatInt([[result1 valueForKeyPath:@"number"] intValue], is(equalToInt(1))); + assertThatFloat([[[result1 valueForKeyPath:@"float"] valueForKey:@"text"] floatValue], is(equalToFloat(2.4f))); + assertThatInt([[[result1 valueForKeyPath:@"number"] valueForKey:@"text"] intValue], is(equalToInt(1))); assertThat([result1 valueForKeyPath:@"string"], is(equalTo(@"string"))); id result2 = [records objectAtIndex:1]; - assertThatInt([[result2 valueForKeyPath:@"another-number"] intValue], is(equalToInt(1))); + assertThatInt([[[result2 valueForKeyPath:@"another-number"] valueForKey:@"text"]intValue], is(equalToInt(1))); } - (void)testShouldMapXML { - RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKSpecTabData class]]; + RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[RKTestTabData class]]; [mapping mapAttributes:@"title", @"summary", nil]; RKObjectMappingProvider* provider = [[RKObjectMappingProvider alloc] init]; - id data = RKSpecParseFixture(@"tab_data.xml"); + id data = [RKTestFixture parsedObjectWithContentsOfFixture:@"tab_data.xml"]; assertThat([data valueForKeyPath:@"tabdata.item"], is(instanceOf([NSArray class]))); [provider setMapping:mapping forKeyPath:@"tabdata.item"]; RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:data mappingProvider:provider]; @@ -87,9 +89,10 @@ - (void)testShouldMapXML { } - (void)testShouldParseXMLWithAttributes { - NSString* XML = RKSpecReadFixture(@"container_attributes.xml"); - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - NSDictionary* result = [parser parseXML:XML]; + NSString* XML = [RKTestFixture stringWithContentsOfFixture:@"container_attributes.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + NSDictionary *result = [parser objectFromString:XML error:&error]; assertThat(result, is(instanceOf([NSDictionary class]))); NSArray* elements = [[result objectForKey:@"elements"] objectForKey:@"element"]; assertThat(elements, isNot(nilValue())); @@ -104,9 +107,10 @@ - (void)testShouldParseXMLWithAttributes { } - (void)testShouldParseXMLWithAttributesInTextNodes { - NSString* XML = RKSpecReadFixture(@"attributes_without_text_content.xml"); - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - NSDictionary* result = [parser parseXML:XML]; + NSString* XML = [RKTestFixture stringWithContentsOfFixture:@"attributes_without_text_content.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + NSDictionary *result = [parser objectFromString:XML error:&error]; NSDictionary* exchangeRate = [result objectForKey:@"exchange_rate"]; assertThat(exchangeRate, is(notNilValue())); assertThat([exchangeRate objectForKey:@"type"], is(equalTo(@"XML_RATE_TYPE_EBNK_MIDDLE"))); @@ -119,13 +123,13 @@ - (void)testShouldParseXMLWithAttributesInTextNodes { assertThat([firstCurrency objectForKey:@"name"], is(equalTo(@"AUD"))); assertThat([firstCurrency objectForKey:@"quota"], is(equalTo(@"1"))); assertThat([firstCurrency objectForKey:@"rate"], is(equalTo(@"18.416"))); - + NSDictionary* secondCurrency = [currency objectAtIndex:1]; assertThat(secondCurrency, is(instanceOf([NSDictionary class]))); assertThat([secondCurrency objectForKey:@"name"], is(equalTo(@"HRK"))); assertThat([secondCurrency objectForKey:@"quota"], is(equalTo(@"1"))); assertThat([secondCurrency objectForKey:@"rate"], is(equalTo(@"3.25017"))); - + NSDictionary* thirdCurrency = [currency objectAtIndex:2]; assertThat(thirdCurrency, is(instanceOf([NSDictionary class]))); assertThat([thirdCurrency objectForKey:@"name"], is(equalTo(@"DKK"))); @@ -134,11 +138,12 @@ - (void)testShouldParseXMLWithAttributesInTextNodes { } - (void)testShouldNotCrashWhileParsingOrdersXML { - NSString *XML = RKSpecReadFixture(@"orders.xml"); - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; + NSString *XML = [RKTestFixture stringWithContentsOfFixture:@"orders.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; NSException *exception = nil; @try { - [parser parseXML:XML]; + [parser objectFromString:XML error:&error];; } @catch (NSException *e) { exception = e; @@ -149,9 +154,10 @@ - (void)testShouldNotCrashWhileParsingOrdersXML { } - (void)testShouldParseXMLWithCDATA { - NSString *XML = RKSpecReadFixture(@"zend.xml"); - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - NSDictionary *output = [parser parseXML:XML]; + NSString *XML = [RKTestFixture stringWithContentsOfFixture:@"zend.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + NSDictionary *output = [parser objectFromString:XML error:&error]; NSArray *map = [output valueForKeyPath:@"Api.getList.map"]; assertThat(map, isNot(nilValue())); assertThat(map, hasCountOf(4)); @@ -162,19 +168,22 @@ - (void)testShouldParseXMLWithCDATA { - (void)testShouldConsiderASingleCloseTagAnEmptyContainer { NSString *XML = @""; - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; - NSDictionary *output = [parser parseXML:XML]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + NSDictionary *output = [parser objectFromString:XML error:&error]; NSDictionary *users = [output valueForKey:@"users"]; + NSLog(@"%@", output); assertThat(users, is(notNilValue())); assertThatBool([users isKindOfClass:[NSDictionary class]], is(equalToBool(YES))); } - (void)testShouldParseRelativelyComplexXML { - NSString *XML = RKSpecReadFixture(@"national_weather_service.xml"); - RKXMLParserLibXML* parser = [[RKXMLParserLibXML new] autorelease]; + NSString *XML = [RKTestFixture stringWithContentsOfFixture:@"national_weather_service.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; NSException *exception = nil; @try { - [parser parseXML:XML]; + [parser objectFromString:XML error:&error]; } @catch (NSException *e) { exception = e; @@ -184,4 +193,46 @@ - (void)testShouldParseRelativelyComplexXML { } } +- (void)testShouldParseXMLElementsAndAttributesProperly { + + NSString* XML = [RKTestFixture stringWithContentsOfFixture:@"channels.xml"]; + NSError *error = [[NSError alloc] init]; + RKXMLParserXMLReader* parser = [[RKXMLParserXMLReader new] autorelease]; + NSDictionary *result = [parser objectFromString:XML error:&error]; + + NSLog(@"result : %@", result); + + NSDictionary *channel = [[result objectForKey:@"Channels"] objectForKey:@"Channel"]; + assertThat(channel, is(notNilValue())); + + // Check to see if the Channel attributes are properly parsed + assertThat([channel objectForKey:@"Identifier"], is(equalTo(@"1172"))); + assertThat([channel objectForKey:@"Title"], is(equalTo(@"MySpecialTitle"))); + assertThat([channel objectForKey:@"Position"], is(equalTo(@"2234"))); + + NSLog(@"channel: %@", channel); + + // Check to see if the Channel elements are properly parsed + assertThat([channel objectForKey:@"Languages"], is(equalTo(@"it"))); + + assertThat([[channel objectForKey:@"Stream"] objectForKey:@"text"], is(equalTo(@"MySpecialTitle"))); + assertThat([[channel objectForKey:@"Stream"] objectForKey:@"Identifier"], is(equalTo(@"MySpecialTitle"))); + assertThat([[channel objectForKey:@"Stream"] objectForKey:@"Index"], is(equalTo(@"0"))); + + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:0] objectForKey:@"Identifier"], is(equalTo(@"42883461"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:0] objectForKey:@"Start"], is(equalTo(@"2011-12-19 20:00:00Z"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:0] objectForKey:@"End"], is(equalTo(@"2011-12-19 21:00:00Z"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:0] objectForKey:@"Title"], is(equalTo(@"Program Title 1"))); + + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:1] objectForKey:@"Identifier"], is(equalTo(@"42883471"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:1] objectForKey:@"Start"], is(equalTo(@"2011-12-19 21:00:00Z"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:1] objectForKey:@"End"], is(equalTo(@"2011-12-19 23:00:00Z"))); + assertThat([[[channel objectForKey:@"Program"] objectAtIndex:1] objectForKey:@"Title"], is(equalTo(@"Program Title"))); + + assertThat([[channel objectForKey:@"Image"] objectAtIndex:0], is(equalTo(@"http://domain.com/Images/MySpecialTitle.png"))); + assertThat([[[channel objectForKey:@"Image"] objectAtIndex:1] objectForKey:@"text"], is(equalTo(@"http://domain.com/Images/65x35/2234.png"))); + assertThat([[[channel objectForKey:@"Image"] objectAtIndex:1] objectForKey:@"Width"], is(equalTo(@"65"))); + assertThat([[[channel objectForKey:@"Image"] objectAtIndex:1] objectForKey:@"Height"], is(equalTo(@"35"))); +} + @end diff --git a/Tests/Models/Data Model.xcdatamodel/elements b/Tests/Models/Data Model.xcdatamodel/elements new file mode 100644 index 0000000000..0a34e7feb5 Binary files /dev/null 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 new file mode 100644 index 0000000000..e3b3a05f40 Binary files /dev/null and b/Tests/Models/Data Model.xcdatamodel/layout differ diff --git a/Specs/Models/RKCat.h b/Tests/Models/RKCat.h similarity index 94% rename from Specs/Models/RKCat.h rename to Tests/Models/RKCat.h index 27bd6304d3..c8e449556a 100644 --- a/Specs/Models/RKCat.h +++ b/Tests/Models/RKCat.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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/Specs/Models/RKCat.m b/Tests/Models/RKCat.m similarity index 92% rename from Specs/Models/RKCat.m rename to Tests/Models/RKCat.m index 857bc647db..e64a913c56 100644 --- a/Specs/Models/RKCat.m +++ b/Tests/Models/RKCat.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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/Specs/Models/RKChild.h b/Tests/Models/RKChild.h similarity index 93% rename from Specs/Models/RKChild.h rename to Tests/Models/RKChild.h index dbd926e8d4..2d95dc71e2 100644 --- a/Specs/Models/RKChild.h +++ b/Tests/Models/RKChild.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 8/25/11. -// Copyright 2011 RestKit -// +// 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/Specs/Models/RKChild.m b/Tests/Models/RKChild.m similarity index 92% rename from Specs/Models/RKChild.m rename to Tests/Models/RKChild.m index d49d3ce37f..042bae3b24 100644 --- a/Specs/Models/RKChild.m +++ b/Tests/Models/RKChild.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 8/25/11. -// Copyright 2011 RestKit -// +// 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/Specs/Models/RKDynamicMappingModels.h b/Tests/Models/RKDynamicMappingModels.h similarity index 92% rename from Specs/Models/RKDynamicMappingModels.h rename to Tests/Models/RKDynamicMappingModels.h index 41a1ca698e..19a2b9fca9 100644 --- a/Specs/Models/RKDynamicMappingModels.h +++ b/Tests/Models/RKDynamicMappingModels.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/11. -// Copyright 2011 RestKit -// +// 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/Specs/Models/RKDynamicMappingModels.m b/Tests/Models/RKDynamicMappingModels.m similarity index 92% rename from Specs/Models/RKDynamicMappingModels.m rename to Tests/Models/RKDynamicMappingModels.m index 95b879aba7..153e78e226 100644 --- a/Specs/Models/RKDynamicMappingModels.m +++ b/Tests/Models/RKDynamicMappingModels.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 7/28/11. -// Copyright 2011 RestKit -// +// 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/RKEvent.h b/Tests/Models/RKEvent.h new file mode 100644 index 0000000000..429b7f4bc9 --- /dev/null +++ b/Tests/Models/RKEvent.h @@ -0,0 +1,33 @@ +// +// RKEvent.h +// RestKit +// +// Created by Greg Combs on 12/23/11. +// Copyright 2011 +// +// 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 "NSManagedObject+ActiveRecord.h" + +@interface RKEvent : NSManagedObject { + +} + +@property (nonatomic, retain) NSString* eventID; +@property (nonatomic, retain) NSString* eventType; +@property (nonatomic, retain) NSString* location; +@property (nonatomic, retain) NSString* summary; + +@end diff --git a/Code/Network/NSData+MD5.h b/Tests/Models/RKEvent.m similarity index 74% rename from Code/Network/NSData+MD5.h rename to Tests/Models/RKEvent.m index db5c4f1ae0..70b62b7444 100644 --- a/Code/Network/NSData+MD5.h +++ b/Tests/Models/RKEvent.m @@ -1,16 +1,16 @@ // -// NSData+MD5.h +// RKEvent.m // RestKit // -// Created by Jeff Arena on 4/4/11. -// Copyright 2011 Two Toasters -// +// Created by Jeremy Ellison on 12/23/11. +// Copyright 2011 +// // 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,8 +19,13 @@ // -@interface NSData (MD5) +#import "RKEvent.h" + +@implementation RKEvent -- (NSString*)MD5; +@dynamic eventID; +@dynamic eventType; +@dynamic location; +@dynamic summary; @end diff --git a/Specs/Models/RKHouse.h b/Tests/Models/RKHouse.h similarity index 94% rename from Specs/Models/RKHouse.h rename to Tests/Models/RKHouse.h index 9feb579484..e13a3735fd 100644 --- a/Specs/Models/RKHouse.h +++ b/Tests/Models/RKHouse.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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/Specs/Models/RKHouse.m b/Tests/Models/RKHouse.m similarity index 92% rename from Specs/Models/RKHouse.m rename to Tests/Models/RKHouse.m index 94cc763c7b..1f3afdbf7c 100644 --- a/Specs/Models/RKHouse.m +++ b/Tests/Models/RKHouse.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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/Specs/Models/RKHuman.h b/Tests/Models/RKHuman.h similarity index 54% rename from Specs/Models/RKHuman.h rename to Tests/Models/RKHuman.h index 68c61a8354..21d1b0ede4 100644 --- a/Specs/Models/RKHuman.h +++ b/Tests/Models/RKHuman.h @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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,20 +22,24 @@ @class RKCat; -@interface RKHuman : NSManagedObject { +@interface RKHuman : NSManagedObject { } -@property (nonatomic, retain) NSNumber* railsID; -@property (nonatomic, retain) NSString* name; -@property (nonatomic, retain) NSString* nickName; -@property (nonatomic, retain) NSDate* birthday; -@property (nonatomic, retain) NSString* sex; -@property (nonatomic, retain) NSNumber* age; -@property (nonatomic, retain) NSDate* createdAt; -@property (nonatomic, retain) NSDate* updatedAt; - -@property (nonatomic, retain) NSSet* cats; -@property (nonatomic, retain) RKCat* favoriteCat; +@property (nonatomic, retain) NSNumber *railsID; +@property (nonatomic, retain) NSString *name; +@property (nonatomic, retain) NSString *nickName; +@property (nonatomic, retain) NSDate *birthday; +@property (nonatomic, retain) NSString *sex; +@property (nonatomic, retain) NSNumber *age; +@property (nonatomic, retain) NSDate *createdAt; +@property (nonatomic, retain) NSDate *updatedAt; +@property (nonatomic, retain) NSArray *favoriteColors; + +@property (nonatomic, retain) NSSet *cats; +@property (nonatomic, retain) RKCat *favoriteCat; + +@property (nonatomic, retain) NSArray *catIDs; +@property (nonatomic, retain) NSOrderedSet *catsInOrderByAge; @end diff --git a/Specs/Models/RKHuman.m b/Tests/Models/RKHuman.m similarity index 84% rename from Specs/Models/RKHuman.m rename to Tests/Models/RKHuman.m index 2178f0a973..e4d175a8c1 100644 --- a/Specs/Models/RKHuman.m +++ b/Tests/Models/RKHuman.m @@ -3,14 +3,14 @@ // RestKit // // Created by Blake Watters on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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,15 @@ @implementation RKHuman @dynamic age; @dynamic createdAt; @dynamic updatedAt; +@dynamic favoriteColors; @dynamic favoriteCat; @dynamic cats; +@dynamic catIDs; +@dynamic catsInOrderByAge; - (NSString*)polymorphicResourcePath { - return @"/this/is/the/path"; + return @"/this/is/the/path"; } @end diff --git a/Specs/Models/RKMappableAssociation.h b/Tests/Models/RKMappableAssociation.h similarity index 90% rename from Specs/Models/RKMappableAssociation.h rename to Tests/Models/RKMappableAssociation.h index daffa38be9..3a41f09626 100644 --- a/Specs/Models/RKMappableAssociation.h +++ b/Tests/Models/RKMappableAssociation.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/17/09. -// Copyright 2009 Two Toasters -// +// 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,7 +21,7 @@ #import @interface RKMappableAssociation : NSObject { - NSString* _testString; + NSString* _testString; NSDate* _date; } diff --git a/Specs/Models/RKMappableAssociation.m b/Tests/Models/RKMappableAssociation.m similarity index 92% rename from Specs/Models/RKMappableAssociation.m rename to Tests/Models/RKMappableAssociation.m index 7e69b4b7ef..804065922d 100644 --- a/Specs/Models/RKMappableAssociation.m +++ b/Tests/Models/RKMappableAssociation.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/17/09. -// Copyright 2009 Two Toasters -// +// 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/Specs/Models/RKMappableObject.h b/Tests/Models/RKMappableObject.h similarity index 83% rename from Specs/Models/RKMappableObject.h rename to Tests/Models/RKMappableObject.h index 17d71af9d6..7abc4cb39d 100644 --- a/Specs/Models/RKMappableObject.h +++ b/Tests/Models/RKMappableObject.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/17/09. -// Copyright 2009 Two Toasters -// +// 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,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/Specs/Models/RKMappableObject.m b/Tests/Models/RKMappableObject.m similarity index 93% rename from Specs/Models/RKMappableObject.m rename to Tests/Models/RKMappableObject.m index fc5547a346..5dd9a9af12 100644 --- a/Specs/Models/RKMappableObject.m +++ b/Tests/Models/RKMappableObject.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 8/17/09. -// Copyright 2009 Two Toasters -// +// 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/Specs/Models/RKObjectLoaderSpecResultModel.h b/Tests/Models/RKObjectLoaderTestResultModel.h similarity index 84% rename from Specs/Models/RKObjectLoaderSpecResultModel.h rename to Tests/Models/RKObjectLoaderTestResultModel.h index f19ad29bdf..3ed220680f 100644 --- a/Specs/Models/RKObjectLoaderSpecResultModel.h +++ b/Tests/Models/RKObjectLoaderTestResultModel.h @@ -1,16 +1,16 @@ // -// RKObjectLoaderSpecResultModel.h +// RKObjectLoaderTestResultModel.h // RestKit // // Created by Blake Watters on 6/23/11. -// Copyright 2011 Two Toasters -// +// 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 -@interface RKObjectLoaderSpecResultModel : NSObject { +@interface RKObjectLoaderTestResultModel : NSObject { NSNumber* _ID; NSDate* _endsAt; NSString* _photoURL; diff --git a/Specs/Models/RKObjectLoaderSpecResultModel.m b/Tests/Models/RKObjectLoaderTestResultModel.m similarity index 78% rename from Specs/Models/RKObjectLoaderSpecResultModel.m rename to Tests/Models/RKObjectLoaderTestResultModel.m index 9ffc8c95bd..16913b39aa 100644 --- a/Specs/Models/RKObjectLoaderSpecResultModel.m +++ b/Tests/Models/RKObjectLoaderTestResultModel.m @@ -1,16 +1,16 @@ // -// RKObjectLoaderSpecResultModel.m +// RKObjectLoaderTestResultModel.m // RestKit // // Created by Blake Watters on 6/23/11. -// Copyright 2011 Two Toasters -// +// 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. @@ -18,10 +18,9 @@ // limitations under the License. // +#import "RKObjectLoaderTestResultModel.h" -#import "RKObjectLoaderSpecResultModel.h" - -@implementation RKObjectLoaderSpecResultModel +@implementation RKObjectLoaderTestResultModel @synthesize ID = _ID; @synthesize endsAt = _endsAt; diff --git a/Specs/Models/RKObjectMapperSpecModel.h b/Tests/Models/RKObjectMapperTestModel.h similarity index 79% rename from Specs/Models/RKObjectMapperSpecModel.h rename to Tests/Models/RKObjectMapperTestModel.h index 0902625aa9..c77b42d5ee 100644 --- a/Specs/Models/RKObjectMapperSpecModel.h +++ b/Tests/Models/RKObjectMapperTestModel.h @@ -1,16 +1,16 @@ // -// RKObjectMapperSpecModel.h +// RKObjectMapperTestModel.h // RestKit // // Created by Blake Watters on 2/18/10. -// Copyright 2010 Two Toasters -// +// 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,10 +21,10 @@ #import -@interface RKObjectMapperSpecModel : NSObject { - NSString* _name; - NSNumber* _age; - NSDate* _createdAt; +@interface RKObjectMapperTestModel : NSObject { + NSString* _name; + NSNumber* _age; + NSDate* _createdAt; } @property (nonatomic,retain) NSString* name; @@ -32,4 +32,3 @@ @property (nonatomic,retain) NSDate* createdAt; @end - diff --git a/Specs/Models/RKObjectMapperSpecModel.m b/Tests/Models/RKObjectMapperTestModel.m similarity index 80% rename from Specs/Models/RKObjectMapperSpecModel.m rename to Tests/Models/RKObjectMapperTestModel.m index 25e00d22a8..8029c569fa 100644 --- a/Specs/Models/RKObjectMapperSpecModel.m +++ b/Tests/Models/RKObjectMapperTestModel.m @@ -1,16 +1,16 @@ // -// RKObjectMapperSpecModel.m +// RKObjectMapperTestModel.m // RestKit // // Created by Blake Watters on 2/18/10. -// Copyright 2010 Two Toasters -// +// 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. @@ -18,9 +18,9 @@ // limitations under the License. // -#import "RKObjectMapperSpecModel.h" +#import "RKObjectMapperTestModel.h" -@implementation RKObjectMapperSpecModel +@implementation RKObjectMapperTestModel @synthesize name = _name; @synthesize age = _age; diff --git a/Specs/Models/RKParent.h b/Tests/Models/RKParent.h similarity index 92% rename from Specs/Models/RKParent.h rename to Tests/Models/RKParent.h index 942c8e85c0..59bcaf7253 100644 --- a/Specs/Models/RKParent.h +++ b/Tests/Models/RKParent.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 8/25/11. -// Copyright 2011 RestKit -// +// 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/Specs/Models/RKParent.m b/Tests/Models/RKParent.m similarity index 91% rename from Specs/Models/RKParent.m rename to Tests/Models/RKParent.m index 3285af6bb7..7ec42ee416 100644 --- a/Specs/Models/RKParent.m +++ b/Tests/Models/RKParent.m @@ -3,14 +3,14 @@ // RestKit // // Created by Jeff Arena on 8/25/11. -// Copyright 2011 RestKit -// +// 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/Specs/Models/RKResident.h b/Tests/Models/RKResident.h similarity index 93% rename from Specs/Models/RKResident.h rename to Tests/Models/RKResident.h index 752eff6cc0..49ce278ce2 100644 --- a/Specs/Models/RKResident.h +++ b/Tests/Models/RKResident.h @@ -3,14 +3,14 @@ // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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/Specs/Models/RKResident.m b/Tests/Models/RKResident.m similarity index 88% rename from Specs/Models/RKResident.m rename to Tests/Models/RKResident.m index b95ab3b62b..82dfc97240 100644 --- a/Specs/Models/RKResident.m +++ b/Tests/Models/RKResident.m @@ -1,16 +1,16 @@ -// +// // RKResident.m // RestKit // // Created by Jeremy Ellison on 1/14/10. -// Copyright 2010 Two Toasters -// +// 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 @@ #import "RKResident.h" #import "RKHouse.h" -@implementation RKResident +@implementation RKResident @dynamic residableType; @dynamic railsID; diff --git a/Tests/Models/RKSearchable.h b/Tests/Models/RKSearchable.h new file mode 100644 index 0000000000..c1e5f93530 --- /dev/null +++ b/Tests/Models/RKSearchable.h @@ -0,0 +1,30 @@ +// +// RKSearchable.h +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import +#import + +@interface RKSearchable : RKSearchableManagedObject + +@property (nonatomic, retain) NSString * title; +@property (nonatomic, retain) NSString * body; + +@end diff --git a/Tests/Models/RKSearchable.m b/Tests/Models/RKSearchable.m new file mode 100644 index 0000000000..13c9501f40 --- /dev/null +++ b/Tests/Models/RKSearchable.m @@ -0,0 +1,33 @@ +// +// RKSearchable.m +// RestKit +// +// 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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RKSearchable.h" + + +@implementation RKSearchable + +@dynamic title; +@dynamic body; + ++ (NSArray*)searchableAttributes { + return [NSArray arrayWithObjects:@"title", @"body", nil]; +} + +@end diff --git a/Tests/Models/RKTestAddress.h b/Tests/Models/RKTestAddress.h new file mode 100644 index 0000000000..0cae94125b --- /dev/null +++ b/Tests/Models/RKTestAddress.h @@ -0,0 +1,25 @@ +// +// RKTestAddress.h +// RestKit +// +// Created by Blake Watters on 8/5/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import + +@interface RKTestAddress : NSObject { + NSNumber* _addressID; + NSString* _city; + NSString* _state; + NSString* _country; +} + +@property (nonatomic, retain) NSNumber* addressID; +@property (nonatomic, retain) NSString* city; +@property (nonatomic, retain) NSString* state; +@property (nonatomic, retain) NSString* country; + ++ (RKTestAddress*)address; + +@end diff --git a/Tests/Models/RKTestAddress.m b/Tests/Models/RKTestAddress.m new file mode 100644 index 0000000000..e05865bbb3 --- /dev/null +++ b/Tests/Models/RKTestAddress.m @@ -0,0 +1,32 @@ +// +// RKTestAddress.m +// RestKit +// +// Created by Blake Watters on 8/5/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestAddress.h" + +@implementation RKTestAddress + +@synthesize addressID = _addressID; +@synthesize city = _city; +@synthesize state = _state; +@synthesize country = _country; + ++ (RKTestAddress*)address { + return [[self new] autorelease]; +} + +// isEqual: is consulted by the mapping operation +// to determine if assocation values should be set +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[RKTestAddress class]]) { + return [[(RKTestAddress*)object addressID] isEqualToNumber:self.addressID]; + } else { + return NO; + } +} + +@end diff --git a/Tests/Models/RKTestUser.h b/Tests/Models/RKTestUser.h new file mode 100644 index 0000000000..528cb7d34f --- /dev/null +++ b/Tests/Models/RKTestUser.h @@ -0,0 +1,49 @@ +// +// RKTestUser.h +// RestKit +// +// Created by Blake Watters on 8/5/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import +#import "RKTestAddress.h" + +@interface RKTestUser : NSObject { + NSNumber* _userID; + NSString* _name; + NSDate* _birthDate; + NSArray* _favoriteColors; + NSDictionary* _addressDictionary; + NSURL* _website; + NSNumber* _isDeveloper; + NSNumber* _luckyNumber; + NSDecimalNumber* _weight; + NSArray* _interests; + NSString* _country; + + // Relationships + RKTestAddress* _address; + NSArray* _friends; +} + +@property (nonatomic, retain) NSNumber* userID; +@property (nonatomic, retain) NSString* name; +@property (nonatomic, retain) NSDate* birthDate; +@property (nonatomic, retain) NSDate *favoriteDate; +@property (nonatomic, retain) NSArray* favoriteColors; +@property (nonatomic, retain) NSDictionary* addressDictionary; +@property (nonatomic, retain) NSURL* website; +@property (nonatomic, retain) NSNumber* isDeveloper; +@property (nonatomic, retain) NSNumber* luckyNumber; +@property (nonatomic, retain) NSDecimalNumber* weight; +@property (nonatomic, retain) NSArray* interests; +@property (nonatomic, retain) NSString* country; +@property (nonatomic, retain) RKTestAddress* address; +@property (nonatomic, retain) NSArray* friends; +@property (nonatomic, retain) NSSet* friendsSet; +@property (nonatomic, retain) NSOrderedSet *friendsOrderedSet; + ++ (RKTestUser*)user; + +@end diff --git a/Tests/Models/RKTestUser.m b/Tests/Models/RKTestUser.m new file mode 100644 index 0000000000..ddb5689f77 --- /dev/null +++ b/Tests/Models/RKTestUser.m @@ -0,0 +1,60 @@ +// +// RKTestUser.m +// RestKit +// +// Created by Blake Watters on 8/5/11. +// Copyright (c) 2009-2012 RestKit. All rights reserved. +// + +#import "RKTestUser.h" +#import "RKLog.h" + +@implementation RKTestUser + +@synthesize userID = _userID; +@synthesize name = _name; +@synthesize birthDate = _birthDate; +@synthesize favoriteDate = _favoriteDate; +@synthesize favoriteColors = _favoriteColors; +@synthesize addressDictionary = _addressDictionary; +@synthesize website = _website; +@synthesize isDeveloper = _isDeveloper; +@synthesize luckyNumber = _luckyNumber; +@synthesize weight = _weight; +@synthesize interests = _interests; +@synthesize country = _country; +@synthesize address = _address; +@synthesize friends = _friends; +@synthesize friendsSet = _friendsSet; +@synthesize friendsOrderedSet = _friendsOrderedSet; + ++ (RKTestUser*)user { + return [[self new] autorelease]; +} + +// isEqual: is consulted by the mapping operation +// to determine if assocation values should be set +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[RKTestUser class]]) { + 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 { + RKLogError(@"Unexpectedly asked for undefined key '%@'", key); + return [super valueForUndefinedKey:key]; +} + +- (void)setValue:(id)value forUndefinedKey:(NSString *)key { + RKLogError(@"Asked to set value '%@' for undefined key '%@'", value, key); + [super setValue:value forUndefinedKey:key]; +} + +@end diff --git a/Tests/RKTestEnvironment.h b/Tests/RKTestEnvironment.h new file mode 100644 index 0000000000..132ba8343f --- /dev/null +++ b/Tests/RKTestEnvironment.h @@ -0,0 +1,47 @@ +// +// RKTestEnvironment.h +// RestKit +// +// Created by Blake Watters on 1/15/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +#define HC_SHORTHAND +#import +#import + +#import +#import +#import + +RKOAuthClient* RKTestNewOAuthClient(RKTestResponseLoader* loader); + +/* + Base class for RestKit test cases. Provides initialization of testing + infrastructure. + */ +@interface RKTestCase : SenTestCase +@end + +@interface SenTestCase (MethodSwizzling) +- (void)swizzleMethod:(SEL)aOriginalMethod + inClass:(Class)aOriginalClass + withMethod:(SEL)aNewMethod + fromClass:(Class)aNewClass + executeBlock:(void (^)(void))aBlock; +@end diff --git a/Tests/RKTestEnvironment.m b/Tests/RKTestEnvironment.m new file mode 100644 index 0000000000..40b7d7086d --- /dev/null +++ b/Tests/RKTestEnvironment.m @@ -0,0 +1,76 @@ +// +// RKTestEnvironment.m +// RestKit +// +// Created by Blake Watters on 3/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. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include +#import "RKTestEnvironment.h" +#import "RKParserRegistry.h" + +RKOAuthClient* RKTestNewOAuthClient(RKTestResponseLoader* loader) +{ + [loader setTimeout:10]; + RKOAuthClient *client = [RKOAuthClient clientWithClientID:@"4fa42a4a7184796662000001" secret:@"restkit_secret"]; + client.delegate = loader; + client.authorizationURL = [NSString stringWithFormat:@"%@/oauth2/pregen/token", [RKTestFactory baseURLString]]; + return client; +} + +@implementation RKTestCase + ++ (void)initialize +{ + // 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 + BOOL directoryExists; + NSError *error = nil; + directoryExists = [RKDirectory ensureDirectoryExistsAtPath:[RKDirectory applicationDataDirectory] error:&error]; + if (! directoryExists) { + RKLogError(@"Failed to create application data directory. Unable to run tests: %@", error); + NSAssert(directoryExists, @"Failed to create application data directory."); + } + + directoryExists = [RKDirectory ensureDirectoryExistsAtPath:[RKDirectory cachesDirectory] error:&error]; + if (! directoryExists) { + RKLogError(@"Failed to create caches directory. Unable to run tests: %@", error); + NSAssert(directoryExists, @"Failed to create caches directory."); + } +} + +@end + +@implementation SenTestCase (MethodSwizzling) + +- (void)swizzleMethod:(SEL)aOriginalMethod + inClass:(Class)aOriginalClass + withMethod:(SEL)aNewMethod + fromClass:(Class)aNewClass + 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/RunPlatformUnitTests b/Tests/RunPlatformUnitTests new file mode 100755 index 0000000000..813da592e1 --- /dev/null +++ b/Tests/RunPlatformUnitTests @@ -0,0 +1,146 @@ +#!/bin/sh +## +# Copyright 2008 Apple Inc. +# All rights reserved. +# +# iPhoneSimulator platform +# This script runs all of the unit tests for the target test bundle specified by the passed-in environment. +# This script is generally intended to be invoked by ${DEVELOPER_TOOLS_DIR}/RunUnitTests. The interface or location of this script may change in future releases. +## +# +# Input: +# See ${DEVELOPER_TOOLS_DIR}/RunUnitTests for input variables + +Message() { +# usage: Message line type message +# Echo the message to stdout as the given type of message and continue + echo "${PLATFORM_DEVELOPER_TOOLS_DIR}/Tools/RunPlatformUnitTests:${1}: ${2}: ${3}" +} + +Note() { +# usage: Notify line message +# Echo the message to stdout as a note and continue + Message "${1}" note "${2}" +} + +Warning() { +# usage: Warning line message +# Echo the message to stdout as a note and continue + Message "${1}" warning "${2}" +} + +Error() { +# usage: Notify line message +# Echo the message to stdout as an error and continue + Message "${1}" error "${2}" +} + +Fail() { +# usage: Fail line message +# Echo the message to stdout and return 1, the universal error code + Error "${1}" "${2}" + exit 1 +} + +### Do not run tests on anything but a "build". + +if [ "${ACTION}" != "build" ]; then + exit 0 +fi + +### Silently skip tests if TEST_AFTER_BUILD is NO + +if [ "${TEST_AFTER_BUILD}" = "NO" ]; then + exit 0 +fi + +### Source RunUnitTests.include functionality + +if [ "${DEVELOPER_TOOLS_DIR}" = "" ]; then + Fail ${LINENO} "DEVELOPER_TOOLS_DIR is not set." +fi + +if [ "${PLATFORM_DEVELOPER_TOOLS_DIR}" = "" ]; then + Fail ${LINENO} "PLATFORM_DEVELOPER_TOOLS_DIR is not set." +fi + +includeFile="${DEVELOPER_TOOLS_DIR}/RunPlatformUnitTests.include" +if [ ! -r "${includeFile}" ]; then + Fail ${LINENO} "Cannot read include file ${includeFile}" +fi + +source "${includeFile}" + +if [ 0 != $? ]; then + Fail ${LINENO} "Could not source include file ${includeFile}" +fi + +### Define a sensible default for the path to the otest + +if [ "${OTEST}" = "" ]; then + OTEST="${SDKROOT}/Developer/usr/bin/otest" +fi + +Main() { +# usage: Main +# Determine how tests need to be run and run them. + + # GC not supported by the simulator + TEST_GC_STATES="OFF" + + Configure_TEST_ARCHS + + if [ "${TEST_HOST}" != "" ]; then + + export CFFIXED_USER_HOME="${BUILT_PRODUCTS_DIR}/UserHome/" + mkdir -p "${CFFIXED_USER_HOME}" + mkdir -p "${CFFIXED_USER_HOME}/Library/Caches" + mkdir "${CFFIXED_USER_HOME}/Library/Preferences" + mkdir "${CFFIXED_USER_HOME}/Documents" + export OTHER_TEST_FLAGS="${OTHER_TEST_FLAGS} -RegisterForSystemEvents" + RunTestsForApplication "${TEST_HOST}" "${TEST_BUNDLE_PATH}" + + else + # If no TEST_HOST is specified, assume we're running the test bundle. + + RunTestsForBundle "${TEST_BUNDLE_PATH}" + fi +} + +### Update the dyld environment to support running tests out of the build directory. + +# Sets and exports the following environment variables: +# DYLD_ROOT_PATH +# DYLD_FRAMEWORK_PATH +# DYLD_LIBRARY_PATH +# DYLD_NEW_LOCAL_SHARED_REGIONS +# DYLD_NO_FIX_PREBINDING + +if [ "${DYLD_FRAMEWORK_PATH}" != "" ]; then + DYLD_FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}:${SDKROOT}${DEVELOPER_LIBRARY_DIR}/Frameworks:${DYLD_FRAMEWORK_PATH}" +else + DYLD_FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}:${SDKROOT}${DEVELOPER_LIBRARY_DIR}/Frameworks" +fi + +if [ "${DYLD_LIBRARY_PATH}" != "" ]; then + DYLD_LIBRARY_PATH="${BUILT_PRODUCTS_DIR}:${DYLD_LIBRARY_PATH}" +else + DYLD_LIBRARY_PATH="${BUILT_PRODUCTS_DIR}" +fi + +if [ "${DYLD_ROOT_PATH}" != "" ]; then + DYLD_ROOT_PATH="${SDKROOT}:${DYLD_ROOT_PATH}" +else + DYLD_ROOT_PATH="${SDKROOT}" +fi + +DYLD_NEW_LOCAL_SHARED_REGIONS=YES +DYLD_NO_FIX_PREBINDING=YES +IPHONE_SIMULATOR_ROOT=$SDKROOT ## rdar://6528939 +CFFIXED_USER_HOME="${HOME}/Library/Application Support/iPhone Simulator/$IPHONESIMULATOR_PLATFORM_VERSION" + +EXPORT_VARS=(DYLD_FRAMEWORK_PATH DYLD_LIBRARY_PATH DYLD_NEW_LOCAL_SHARED_REGIONS DYLD_NO_FIX_PREBINDING DYLD_ROOT_PATH IPHONE_SIMULATOR_ROOT CFFIXED_USER_HOME ) + +### Run the tests + +Main 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/Specs/Server/lib/restkit.rb b/Tests/Server/lib/restkit.rb similarity index 98% rename from Specs/Server/lib/restkit.rb rename to Tests/Server/lib/restkit.rb index ad7764eaac..be9281d214 100644 --- a/Specs/Server/lib/restkit.rb +++ b/Tests/Server/lib/restkit.rb @@ -1,3 +1,2 @@ require 'restkit/network/authentication' require 'restkit/network/oauth2' - diff --git a/Specs/Server/lib/restkit/network/authentication.rb b/Tests/Server/lib/restkit/network/authentication.rb similarity index 96% rename from Specs/Server/lib/restkit/network/authentication.rb rename to Tests/Server/lib/restkit/network/authentication.rb index 044e4676ff..a74b7ee354 100644 --- a/Specs/Server/lib/restkit/network/authentication.rb +++ b/Tests/Server/lib/restkit/network/authentication.rb @@ -5,11 +5,11 @@ class Authentication < Sinatra::Base AUTH_PASSWORD = 'authentication' AUTH_REALM = 'RestKit' AUTH_OPAQUE = '7e7e7e7e7e' - + get '/authentication/none' do "Success!" end - + get '/authentication/basic' do @auth ||= Rack::Auth::Basic::Request.new(request.env) puts "Auth was provided: #{@auth.provided?}" @@ -19,7 +19,7 @@ class Authentication < Sinatra::Base throw(:halt, [401, "Access Denied.\n"]) end end - + get '/authentication/digest' do app = lambda do |env| [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ] @@ -31,7 +31,7 @@ class Authentication < Sinatra::Base auth.opaque = AUTH_OPAQUE auth.passwords_hashed = true auth.call(request.env) - end + end end end end diff --git a/Specs/Server/lib/restkit/network/etags.rb b/Tests/Server/lib/restkit/network/etags.rb similarity index 93% rename from Specs/Server/lib/restkit/network/etags.rb rename to Tests/Server/lib/restkit/network/etags.rb index 5d0303498f..21ae88e74d 100644 --- a/Specs/Server/lib/restkit/network/etags.rb +++ b/Tests/Server/lib/restkit/network/etags.rb @@ -1,11 +1,11 @@ module RestKit module Network class ETags < Sinatra::Base - + get '/etags' do "Success!" end - + get '/etags/cached' do tag = "686897696a7c876b7e" if tag == request.env["HTTP_IF_NONE_MATCH"] @@ -16,7 +16,7 @@ class ETags < Sinatra::Base "This Should Get Cached" end end - + end end end 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 new file mode 100644 index 0000000000..d9e5a4b3b4 --- /dev/null +++ b/Tests/Server/lib/restkit/network/oauth2.rb @@ -0,0 +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 + 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) + + @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 + end + + 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/Specs/Server/lib/restkit/network/timeout.rb b/Tests/Server/lib/restkit/network/timeout.rb similarity index 90% rename from Specs/Server/lib/restkit/network/timeout.rb rename to Tests/Server/lib/restkit/network/timeout.rb index 4ede8419ce..6cf083959f 100644 --- a/Specs/Server/lib/restkit/network/timeout.rb +++ b/Tests/Server/lib/restkit/network/timeout.rb @@ -1,11 +1,11 @@ module RestKit module Network class Timeout < Sinatra::Base - + get '/disk/cached' do "This Should Get Cached For 5 Seconds" end - + end end end diff --git a/Specs/Server/server.rb b/Tests/Server/server.rb similarity index 56% rename from Specs/Server/server.rb rename to Tests/Server/server.rb index 4f7a14935b..3169a84453 100644 --- a/Specs/Server/server.rb +++ b/Tests/Server/server.rb @@ -1,123 +1,150 @@ #!/usr/bin/env ruby -# RestKit Spec Server +# RestKit Test Server require 'rubygems' +require 'bundler/setup' require 'sinatra/base' -require "sinatra/reloader" require 'json' -require 'ruby-debug' -Debugger.start +begin + require 'ruby-debug' + Debugger.start +rescue LoadError + # No debugging... +end + +ENV["DB"] = "rack_oauth2_server" -# Import the RestKit Spec 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) + {:name => name, :age => age}.to_json + end +end -class RestKit::SpecServer < Sinatra::Base - self.app_file = __FILE__ +class RestKitTestServer < Sinatra::Base + self.app_file = __FILE__ + + configure do + enable :logging, :dump_errors + set :public_folder, Proc.new { File.expand_path(File.join(root, '../Fixtures')) } + set :uploads_path, Proc.new { File.expand_path(File.join(root, '../Fixtures/Uploads')) } + end + use RestKit::Network::Authentication use RestKit::Network::ETags use RestKit::Network::Timeout - use RestKit::Network::OAuth2 - - configure do - register Sinatra::Reloader - set :logging, true - set :dump_errors, true - set :public, Proc.new { File.join(root, '../Fixtures') } - set :uploads_path, Proc.new { File.join(root, '../Fixtures/Uploads') } + use RestKit::Network::Redirection + + def render_fixture(path, options = {}) + send_file File.join(settings.public_folder, path), options end - + get '/' do content_type 'application/json' {'status' => 'ok'}.to_json end - + post '/photo' do content_type 'application/json' "OK" end - + get '/errors.json' do - status 401 content_type 'application/json' - send_file 'Specs/Server/../Fixtures/JSON/errors.json' + render_fixture('/JSON/errors.json', :status => 400) end - + post '/humans' do status 201 content_type 'application/json' puts "Got params: #{params.inspect}" {:human => {:name => "My Name", :id => 1, :website => "http://restkit.org/"}}.to_json end - + post '/humans/fail' do - status 500 content_type 'application/json' - send_file 'Specs/Server/../Fixtures/JSON/errors.json' + render_fixture('/JSON/errors.json', :status => 500) end - + get '/humans/1' do status 200 content_type 'application/json' puts "Got params: #{params.inspect}" {:human => {:name => "Blake Watters", :id => 1}}.merge(params).to_json end - + delete '/humans/1' do status 200 content_type 'application/json' "{}" end - + post '/echo_params' do status 200 content_type 'application/json' params.to_json end - + post '/204' do status 204 content_type 'application/json' - "" + "".to_json end + get '/204' do + status 204 + content_type 'application/json' + "".to_json + end + get '/403' do status 403 content_type 'application/json' "{}" end - + get '/404' do status 404 content_type 'text/html' "File Not Found" end - + + get '/503' do + status 503 + "Internal Server Error" + end + get '/encoding' do status 200 content_type 'text/plain; charset=us-ascii' "ASCII Charset" end - + post '/notNestedUser' do content_type 'application/json' { 'email' => 'changed', 'ID' => 31337 }.to_json end - + delete '/humans/1234' do content_type 'application/json' status 200 end - + get '/users/empty' do content_type 'application/json' status 200 { :firstUser => {}, :secondUser => {}}.to_json end - + put '/ping' do status 200 content_type 'application/json' @@ -133,24 +160,36 @@ class RestKit::SpecServer < Sinatra::Base params.to_json end + post '/timeout' do + sleep 2 + status 200 + content_type 'application/json' + params.to_json + end + get '/empty/array' do status 200 content_type 'application/json' [].to_json end - + get '/empty/dictionary' do status 200 content_type 'application/json' {}.to_json end - + get '/empty/string' do status 200 content_type 'application/json' "" end - + + get '/fail' do + content_type 'application/json' + render_fixture('/JSON/errors.json', :status => 500) + end + # Expects an uploaded 'file' param post '/upload' do unless params['file'] @@ -164,6 +203,7 @@ class RestKit::SpecServer < Sinatra::Base status 200 "Uploaded successfully to '#{upload_path}'" end + # Return 200 after a delay get '/ok-with-delay/:delay' do sleep params[:delay].to_f @@ -172,6 +212,51 @@ class RestKit::SpecServer < Sinatra::Base "" end + get '/paginate' do + status 200 + content_type 'application/json' + + per_page = 3 + total_entries = 6 + current_page = params[:page].to_i + entries = [] + + puts "Params are: #{params.inspect}. CurrentPage = #{current_page}" + + case current_page + when 1 + entries << Person.new('Blake', 29) + entries << Person.new('Sarah', 30) + entries << Person.new('Colin', 27) + when 2 + entries << Person.new('Asia', 8) + entries << Person.new('Roy', 2) + entries << Person.new('Lola', 9) + when 3 + # Return an error payload + status 422 + return {:error => "Invalid page number."} + else + status 404 + return "" + end + + {:per_page => per_page, :total_entries => total_entries, + :current_page => current_page, :entries => entries}.to_json + end + + get '/coredata/etag' do + content_type 'application/json' + tag = '2cdd0a2b329541d81e82ab20aff6281b' + if tag == request.env["HTTP_IF_NONE_MATCH"] + status 304 + "" + else + etag(tag) + render_fixture '/JSON/humans/all.json' + end + end + # start the server if ruby file executed directly run! if app_file == $0 end 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/Specs/Runner/OCHamcrest/OCHamcrest.framework/Headers b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Headers similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Headers rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Headers diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/OCHamcrest b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/OCHamcrest similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/OCHamcrest rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/OCHamcrest diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Resources b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Resources similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Resources rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Resources diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h index b03701e83c..aa8458b08f 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAllOf.h @@ -24,18 +24,18 @@ OBJC_EXPORT id HC_allOf(id match, ...) NS_REQUIRES_NIL_TERMINATION; /** allOf(firstMatcher, ...) - Matches if all of the given matchers evaluate to @c YES. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + The matchers are evaluated from left to right using short-circuit evaluation, so evaluation stops as soon as a matcher returns @c NO. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_allOf instead.) - + @ingroup logical_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h index 9f7f38ec83..7043dc65f5 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAnyOf.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_anyOf(id match, ...) NS_REQUIRES_NIL_TERMINATION; /** anyOf(firstMatcher, ...) - Matches if any of the given matchers evaluate to @c YES. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + The matchers are evaluated from left to right using short-circuit evaluation, so evaluation stops as soon as a matcher returns @c YES. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anyOf instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h index d699f8279b..ea50cab095 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCAssertThat.h @@ -19,20 +19,20 @@ OBJC_EXPORT void HC_assertThatWithLocation(id testCase, id actual, id /** assertThat(actual, matcher) - Asserts that actual value satisfies matcher. - + @param actual The object to evaluate as the actual value. @param matcher The matcher to satisfy as the expected condition. - + @c assertThat passes the actual value to the matcher for evaluation. If the matcher is not satisfied, an exception is thrown describing the mismatch. - + @c assertThat is designed to integrate well with OCUnit and other unit testing frameworks. Unmet assertions are reported as test failures. In Xcode, these failures can be clicked to reveal the line of the assertion. - + In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_assertThat instead. - + @ingroup integration */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h index af666d9c50..e702fd5897 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseDescription.h @@ -11,7 +11,7 @@ /** Base class for all HCDescription implementations. - + @ingroup core */ @interface HCBaseDescription : NSObject diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseMatcher.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBaseMatcher.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBoxNumber.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBoxNumber.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBoxNumber.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCBoxNumber.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h index 334fe1a260..c1a9a038e1 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCCollectMatchers.h @@ -15,7 +15,7 @@ /** Returns an array of matchers from a variable-length comma-separated list terminated by @c nil. - + @ingroup helpers */ OBJC_EXPORT NSMutableArray *HCCollectMatchers(id item1, va_list args); diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h index 196959e493..717d173160 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescribedAs.h @@ -31,16 +31,16 @@ OBJC_EXPORT id HC_describedAs(NSString *description, id ma /** describedAs(description, matcher, ...) - Adds custom failure description to a given matcher. - + @param description Overrides the matcher's description. @param matcher,... The matcher to satisfy, followed by a comma-separated list of substitution values ending with @c nil. - + The description may contain substitution placeholders \%0, \%1, etc. These will be replaced by any values that follow the matcher. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_describedAs instead.) - + @ingroup decorator_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h index 7b3d13212b..26296d38bf 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h @@ -10,7 +10,7 @@ /** A description of an HCMatcher. - + An HCMatcher will describe itself to a description which can later be used for reporting. @ingroup core @@ -19,32 +19,32 @@ /** Appends some plain text to the description. - + @return @c self, for chaining. */ - (id)appendText:(NSString *)text; /** Appends description of given value to @c self. - + If the value implements the @ref HCSelfDescribing protocol, then it will be used. - + @return @c self, for chaining. */ - (id)appendDescriptionOf:(id)value; /** Appends an arbitary value to the description. - + @b Deprecated: Call @ref appendDescriptionOf: instead. @return @c self, for chaining. */ - (id)appendValue:(id)value __attribute__((deprecated)); -/** +/** Appends a list of objects to the description. - + @return @c self, for chaining. */ - (id)appendList:(NSArray *)values diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h index 35751fddc3..292b0fa973 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasCount.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_hasCount(id matcher); Matches if object's @c -count satisfies a given matcher. @param aMatcher The matcher to satisfy. - + This matcher invokes @c -count on the evaluated object to get the number of elements it contains, passing the result to @a aMatcher for evaluation. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasCount instead.) @@ -47,10 +47,10 @@ OBJC_EXPORT id HC_hasCountOf(NSUInteger count); Matches if object's @c -count equals a given value. @param value @c NSUInteger value to compare against as the expected value. - + This matcher invokes @c -count on the evaluated object to get the number of elements it contains, comparing the result to @a value for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasCountOf instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h index fb3046683d..2a2295ea87 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasDescription.h @@ -23,11 +23,11 @@ OBJC_EXPORT id HC_hasDescription(id match); Matches if object's @c -description satisfies a given matcher. @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. - + This matcher invokes @c -description on the evaluated object to get its description, passing the result to a given matcher for evaluation. If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasDescription(@ref startsWith(\@"foo")) @li @ref hasDescription(\@"bar") diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h index 354e1bf44b..cd1bbd465e 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCHasProperty.h @@ -25,23 +25,23 @@ OBJC_EXPORT id HC_hasProperty(NSString *name, id valueMatch); /** hasProperty(name, valueMatcher) - Matches if object has a method of a given name whose return value satisfies a given matcher. - + @param name The name of a method without arguments that returns an object. @param valueMatcher The matcher to satisfy for the return value, or an expected value for @ref equalTo matching. - + This matcher first checks if the evaluated object has a method with a name matching the given @c name. If so, it invokes the method and sees if the returned value satisfies @c valueMatcher. - + While this matcher is called "hasProperty", it's useful for checking the results of any simple methods, not just properties. - + Examples: @li @ref hasProperty(@"firstName", @"Joe") @li @ref hasProperty(@"firstName", startsWith(@"J")) - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasProperty instead.) - + @ingroup object_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h index c1c03024b2..b9604ecb1a 100755 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCInvocationMatcher.h @@ -10,7 +10,7 @@ /** Supporting class for matching a feature of an object. - + Tests whether the result of passing a given invocation to the value satisfies a given matcher. @ingroup helpers @@ -24,7 +24,7 @@ /** Determines whether a mismatch will be described in short form. - + Default is long form, which describes the object, the name of the invocation, and the sub-matcher's mismatch diagnosis. Short form only has the sub-matcher's mismatch diagnosis. */ @@ -39,7 +39,7 @@ /** Helper method for creating an invocation. - + @b Deprecated: Use new name +invocationForSelector:onClass: */ + (NSInvocation *)createInvocationForSelector:(SEL)selector onClass:(Class)aClass __attribute__((deprecated)); diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h index 0ae3e32205..21d14a57e5 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIs.h @@ -28,7 +28,7 @@ OBJC_EXPORT id HC_is(id match); @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. This matcher compares the evaluated object to the given matcher. - + If the @a aMatcher argument is a matcher, its behavior is retained, but the test may be more expressive. For example: @li @ref assertThatInt(value, equalToInt(5)) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h similarity index 93% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h index 371276f07f..2b39f7e736 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsAnything.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_anything(); /** Matches anything. - - This matcher always evaluates to @c YES. Specify this in composite matchers when the value of a + + This matcher always evaluates to @c YES. Specify this in composite matchers when the value of a particular element is unimportant. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anything instead.) @@ -45,12 +45,12 @@ OBJC_EXPORT id HC_anythingWithDescription(NSString *aDescription); /** anythingWithDescription(description) - Matches anything. - + @param description A string used to describe this matcher. - - This matcher always evaluates to @c YES. Specify this in collection matchers when the value of a + + This matcher always evaluates to @c YES. Specify this in collection matchers when the value of a particular element in a collection is unimportant. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anything instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h index 1c2a9d6833..8e26d155c1 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCloseTo.h @@ -25,13 +25,13 @@ OBJC_EXPORT id HC_closeTo(double aValue, double aDelta); /** closeTo(aValue, aDelta) - Matches if object is a number close to a given value, within a given delta. - + @param aValue The @c double value to compare against as the expected value. @param aDelta The @c double maximum delta between the values for which the numbers are considered close. - + This matcher invokes @c -doubleValue on the evaluated object to get its value as a @c double. The result is compared against @a aValue to see if the difference is within a positive @a aDelta. - + Example: @li @ref closeTo(3.0, 0.25) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h index dd749b2720..a08d59b05d 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContaining.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_hasItem(id itemMatch); /** hasItem(aMatcher) - Matches if any element of collection satisfies a given matcher. - + @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated collection, searching for any element that satisfies a given matcher. If a matching element is found, @c hasItem is satisfied. - + If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasItem instead.) @@ -48,15 +48,15 @@ OBJC_EXPORT id HC_hasItems(id itemMatch, ...) NS_REQUIRES_NIL_TERMINA /** hasItems(firstMatcher, ...) - Matches if all of the given matchers are satisfied by any elements of the collection. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the given matchers, searching for any elements in the evaluated collection that satisfy them. If each matcher is satisfied, then @c hasItems is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c hasItems instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h similarity index 96% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h index 98688e8815..9e7219671e 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h @@ -24,17 +24,17 @@ OBJC_EXPORT id HC_containsInAnyOrder(id itemMatch, ...) NS_REQUIRES_N /** containsInAnyOrder(firstMatcher, ...) - Matches if collection's elements, in any order, satisfy a given list of matchers. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection, seeing if each element satisfies any of the given matchers. The matchers are tried from left to right, and when a satisfied matcher is - found, it is no longer a candidate for the remaining elements. If a one-to-one correspondence is + found, it is no longer a candidate for the remaining elements. If a one-to-one correspondence is established between elements and matchers, @c containsInAnyOrder is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_containsInAnyOrder instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h index 0cf812ddf4..37f8b4719d 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_contains(id itemMatch, ...) NS_REQUIRES_NIL_TERMINA /** contains(firstMatcher, ...) - Matches if collection's elements satisfy a given list of matchers, in order. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection and a given list of matchers, seeing if each element satisfies its corresponding matcher. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_contains instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h index 3cf0b3a951..aa20d49236 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h @@ -24,22 +24,22 @@ OBJC_EXPORT id HC_onlyContains(id itemMatch, ...) NS_REQUIRES_NIL_TER /** onlyContains(firstMatcher, ...) - Matches if each element of collection satisfies any of the given matchers. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection, confirming whether each element satisfies any of the given matchers. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. Example: - + @par @ref onlyContains(startsWith(@"Jo"), nil) - + will match a collection [@"Jon", @"John", @"Johann"]. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_onlyContains instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h index 61e623a5b1..5639b8e336 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContaining.h @@ -28,16 +28,16 @@ OBJC_EXPORT id HC_hasEntry(id keyMatch, id valueMatch); /** hasEntry(keyMatcher, valueMatcher) - Matches if dictionary contains key-value entry satisfying a given pair of matchers. - + @param keyMatcher The matcher to satisfy for the key, or an expected value for @ref equalTo matching. @param valueMatcher The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry that satisfies @a keyMatcher and @a valueMatcher. If a matching entry is found, @c hasEntry is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasEntry(@ref equalTo(@"foo"), equalTo(@"bar")) @li @ref hasEntry(@"foo", @"bar") diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h index 394e4c768c..475b5bb197 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h @@ -29,10 +29,10 @@ OBJC_EXPORT id HC_hasEntries(id keysAndValueMatch, ...) NS_REQUIRES_N hasEntries(firstKey, valueMatcher, ...) - Matches if dictionary contains entries satisfying a list of alternating keys and their value matchers. - + @param firstKey A key (not a matcher) to look up. @param valueMatcher,... The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + Note that the keys must be actual keys, not matchers. Any value argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. The list must end with @c nil. @@ -40,7 +40,7 @@ OBJC_EXPORT id HC_hasEntries(id keysAndValueMatch, ...) NS_REQUIRES_N Examples: @li @ref hasEntries(@"first", equalTo(@"Jon"), @"last", equalTo(@"Reid"), nil) @li @ref hasEntries(@"first", @"Jon", @"last", @"Reid", nil) - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasEntry instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h index 0b03219804..ea876365c5 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h @@ -24,22 +24,22 @@ OBJC_EXPORT id HC_hasKey(id keyMatch); /** hasKey(keyMatcher) - Matches if dictionary contains an entry whose key satisfies a given matcher. - + @param keyMatcher The matcher to satisfy for the key, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry whose key satisfies the given matcher. If a matching entry is found, @c hasKey is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasKey instead.) Examples: @li @ref hasEntry(equalTo(@"foo")) @li @ref hasEntry(@"foo") - + @ingroup collection_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h index 728756a9ca..e2e8afc84b 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h @@ -24,19 +24,19 @@ OBJC_EXPORT id HC_hasValue(id valueMatch); /** hasValue(valueMatcher) - Matches if dictionary contains an entry whose value satisfies a given matcher. - + @param valueMatcher The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry whose value satisfies the given matcher. If a matching entry is found, @c hasValue is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasValue(equalTo(@"bar")) @li @ref hasValue(@"bar") - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasValue instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h index a593ba90cc..c6e13c6904 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEmptyCollection.h @@ -23,7 +23,7 @@ OBJC_EXPORT id HC_empty(); This matcher invokes @c -count on the evaluated object to determine if the number of elements it contains is zero. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_empty instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h index e848a1262c..6644f666e0 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqual.h @@ -24,12 +24,12 @@ OBJC_EXPORT id HC_equalTo(id object); /** equalTo(anObject) - Matches if object is equal to a given object. - + @param anObject The object to compare against as the expected value. - + This matcher compares the evaluated object to @a anObject for equality, as determined by the @c -isEqual: method. - + If @a anObject is @c nil, the matcher will successfully match @c nil. (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h similarity index 95% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h index 4aaaa976bd..5f55214030 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h @@ -26,15 +26,15 @@ OBJC_EXPORT id HC_equalToIgnoringCase(NSString *aString); Matches if object is a string equal to a given string, ignoring case differences. @param aString The string to compare against as the expected value. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it compares it with + + This matcher first checks whether the evaluated object is a string. If so, it compares it with @a aString, ignoring differences of case. - + Example: - + @par @ref equalToIgnoringCase(@"hello world") - + will match "heLLo WorlD". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h similarity index 95% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h index a570d8063d..0a0735dbfa 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h @@ -27,15 +27,15 @@ OBJC_EXPORT id HC_equalToIgnoringWhiteSpace(NSString *aString); Matches if object is a string equal to a given string, ignoring differences in whitespace. @param aString The string to compare against as the expected value. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it compares it with + + This matcher first checks whether the evaluated object is a string. If so, it compares it with @a aString, ignoring differences in runs of whitespace. - + Example: - + @par @ref equalToIgnoringWhiteSpace(@"hello world") - + will match @verbatim "hello world" @endverbatim (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h similarity index 91% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h index 9dd508c104..74a70c6b40 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsEqualToNumber.h @@ -13,12 +13,12 @@ OBJC_EXPORT id HC_equalToBool(BOOL value); /** equalToBool(value) - Matches if object is equal to @c NSNumber created from a @c BOOL. - + @param value The @c BOOL value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c BOOL @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToBool instead.) @@ -34,12 +34,12 @@ OBJC_EXPORT id HC_equalToChar(char value); /** equalToChar(value) - Matches if object is equal to @c NSNumber created from a @c char. - + @param value The @c char value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c char @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToChar instead.) @@ -55,12 +55,12 @@ OBJC_EXPORT id HC_equalToDouble(double value); /** equalToDouble(value) - Matches if object is equal to @c NSNumber created from a @c double. - + @param value The @c double value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c double @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToDouble instead.) @@ -76,12 +76,12 @@ OBJC_EXPORT id HC_equalToFloat(float value); /** equalToFloat(value) - Matches if object is equal to @c NSNumber created from a @c float. - + @param value The @c float value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c float @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToFloat instead.) @@ -97,12 +97,12 @@ OBJC_EXPORT id HC_equalToInt(int value); /** equalToInt(value) - Matches if object is equal to @c NSNumber created from an @c int. - + @param value The @c int value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c int @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToInt instead.) @@ -118,12 +118,12 @@ OBJC_EXPORT id HC_equalToLong(long value); /** equalToLong(value) - Matches if object is equal to @c NSNumber created from a @c long. - + @param value The @c long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c long @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToLong instead.) @@ -139,12 +139,12 @@ OBJC_EXPORT id HC_equalToLongLong(long long value); /** equalToLongLong(value) - Matches if object is equal to @c NSNumber created from a long long. - + @param value The long long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a long long @a value and compares - the evaluated object to it for equality. - + the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToLongLong instead.) @@ -160,12 +160,12 @@ OBJC_EXPORT id HC_equalToShort(short value); /** equalToShort(value) - Matches if object is equal to @c NSNumber created from a @c short. - + @param value The @c short value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c short @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToShort instead.) @@ -181,12 +181,12 @@ OBJC_EXPORT id HC_equalToUnsignedChar(unsigned char value); /** equalToUnsignedChar(value) - Matches if object is equal to @c NSNumber created from an unsigned char. - + @param value The unsigned char value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned char @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedChar instead.) @@ -202,12 +202,12 @@ OBJC_EXPORT id HC_equalToUnsignedInt(unsigned int value); /** equalToUnsignedInt(value) - Matches if object is equal to @c NSNumber created from an unsigned int. - + @param value The unsigned int value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned int @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedInt instead.) @@ -223,12 +223,12 @@ OBJC_EXPORT id HC_equalToUnsignedLong(unsigned long value); /** equalToUnsignedLong(value) - Matches if object is equal to @c NSNumber created from an unsigned long. - + @param value The unsigned long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned long @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedLong instead.) @@ -244,12 +244,12 @@ OBJC_EXPORT id HC_equalToUnsignedLongLong(unsigned long long value); /** equalToUnsignedLongLong(value) - Matches if object is equal to @c NSNumber created from an unsigned long long. - + @param value The unsigned long long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned long long @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedLongLong instead.) @@ -265,12 +265,12 @@ OBJC_EXPORT id HC_equalToUnsignedShort(unsigned short value); /** equalToUnsignedShort(value) - Matches if object is equal to @c NSNumber created from an unsigned short. - + @param value The unsigned short value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned short @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedShort instead.) @@ -286,12 +286,12 @@ OBJC_EXPORT id HC_equalToInteger(NSInteger value); /** equalToInteger(value) - Matches if object is equal to @c NSNumber created from an @c NSInteger. - + @param value The @c NSInteger value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an @c NSInteger @a value and compares the - evaluated object to it for equality. - + evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToInteger instead.) @@ -307,12 +307,12 @@ OBJC_EXPORT id HC_equalToUnsignedInteger(NSUInteger value); /** equalToUnsignedInteger(value) - Matches if object is equal to @c NSNumber created from an @c NSUInteger. - + @param value The @c NSUInteger value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an @c NSUInteger @a value and compares the - evaluated object to it for equality. - + evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedInteger instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h index 8a90f785d5..2200a2e1fe 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsIn.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_isIn(id aCollection); Matches if evaluated object is present in a given collection. @param aCollection The collection to search. - + This matcher invokes @c -containsObject: on @a aCollection to determine if the evaluated object is an element of the collection. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_isIn instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h index a3df8f2433..87b3065ac3 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsInstanceOf.h @@ -24,12 +24,12 @@ OBJC_EXPORT id HC_instanceOf(Class aClass); /** instanceOf(aClass) - Matches if object is an instance of, or inherits from, a given class. - + @param aClass The class to compare against as the expected class. - + This matcher checks whether the evaluated object is an instance of @a aClass or an instance of any class that inherits from @a aClass. - + Example: @li @ref instanceOf([NSString class]) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h index 871c74558b..0e6ed27397 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNil.h @@ -34,7 +34,7 @@ OBJC_EXPORT id HC_notNilValue(); /** Matches if object is not @c nil. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_notNilValue instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h index e9e4ebb771..dde5e3de8f 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsNot.h @@ -26,7 +26,7 @@ OBJC_EXPORT id HC_isNot(id aMatcher); Inverts the given matcher to its logical negation. @param aMatcher The matcher to negate. - + This matcher compares the evaluated object to the negation of the given matcher. If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality, and thus matches for inequality. diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h index 52787f64c3..fecc0099a1 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCIsSame.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_sameInstance(id object); Matches if evaluated object is the same instance as a given object. @param anObject The object to compare against as the expected value. - + This matcher compares the address of the evaluated object to determine if it is the same object as @a anObject. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_sameInstance instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h index 7fae682876..85d3417e6b 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCMatcher.h @@ -10,9 +10,9 @@ /** A matcher over acceptable values. - + A matcher is able to describe itself to give feedback when it fails. - + HCMatcher implementations should @b not directly implement this protocol. Instead, @b extend the HCBaseMatcher class, which will ensure that the HCMatcher API can grow to support new features and remain compatible with all HCMatcher implementations. @@ -40,12 +40,12 @@ /** Generates a description of why the matcher has not accepted the item. - + The description will be part of a larger description of why a matching failed, so it should be concise. - + This method assumes that @c matches:item is false, but will not check this. - + @param item The item that the HCMatcher has rejected. @param mismatchDescription The description to be built or appended to. */ diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCNumberAssert.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCNumberAssert.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCNumberAssert.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCNumberAssert.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h index 0767f48b02..efd85f6b0a 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCOrderingComparison.h @@ -34,9 +34,9 @@ OBJC_EXPORT id HC_greaterThan(id expected); /** greaterThan(aNumber) - Matches if object is greater than a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref greaterThan([NSNumber numberWithInt:5]) @@ -55,9 +55,9 @@ OBJC_EXPORT id HC_greaterThanOrEqualTo(id expected); /** greaterThanOrEqualTo(aNumber) - Matches if object is greater than or equal to a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref greaterThanOrEqualTo([NSNumber numberWithInt:5]) @@ -76,9 +76,9 @@ OBJC_EXPORT id HC_lessThan(id expected); /** lessThan(aNumber) - Matches if object is less than a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref lessThan([NSNumber numberWithInt:5]) @@ -97,9 +97,9 @@ OBJC_EXPORT id HC_lessThanOrEqualTo(id expected); /** lessThanOrEqualTo(aNumber) - Matches if object is less than or equal to a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref lessThanOrEqualTo([NSNumber numberWithInt:5]) diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h index 0643345183..2ec9b94258 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilObject.h @@ -11,7 +11,7 @@ /** Throws an NSException if @a obj is @c nil. - + @ingroup helpers */ OBJC_EXPORT void HCRequireNonNilObject(id obj); diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h index 5d887f1484..ee1aa723fc 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h @@ -11,9 +11,9 @@ /** Throws an NSException if @a string is @c nil. - + @b Deprecated: Use @ref HCRequireNonNilObject instead. - + @ingroup helpers */ OBJC_EXPORT void HCRequireNonNilString(NSString *string) __attribute__((deprecated)); diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h index 0acb1bb662..31059edac8 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSelfDescribing.h @@ -19,10 +19,10 @@ /** Generates a description of the object. - + The description may be part of a description of a larger object of which this is just a component, so it should be worded appropriately. - + @param description The description to be built or appended to. */ - (void)describeTo:(id)description; diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h similarity index 94% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h index 7e4a8becf6..72e50b47ff 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContains.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_containsString(NSString *aSubstring); Matches if object is a string containing a given string. @param aString The string to search for. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it checks whether it + + This matcher first checks whether the evaluated object is a string. If so, it checks whether it contains @a aString. - + Example: - + @par @ref containsString(@"def") - + will match "abcdefg". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h similarity index 93% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h index a5eec3bcbf..fe75bb0e76 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringContainsInOrder.h @@ -26,11 +26,11 @@ OBJC_EXPORT id HC_stringContainsInOrder(NSString *substring, ...) NS_ Matches if object is a string containing a given list of substrings in relative order. @param firstString,... A comma-separated list of strings ending with @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it checks whether it - contains a given list of strings, in relative order to each other. The searches are performed + + This matcher first checks whether the evaluated object is a string. If so, it checks whether it + contains a given list of strings, in relative order to each other. The searches are performed starting from the beginning of the evaluated string. - + Example: @par diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringDescription.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringDescription.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h index 97ab632d86..39bfbda6c4 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringEndsWith.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_endsWith(NSString *aSubstring); Matches if object is a string ending with a given string. @param aString The string to search for. This value must not be @c nil. - + This matcher first checks whether the evaluated object is a string. If so, it checks if @a aString matches the ending characters of the evaluated object. - + Example: - + @par @ref endsWith(@"bar") - + will match "foobar". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h index a59baa4a9d..2620d84c86 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCStringStartsWith.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_startsWith(NSString *aSubstring); Matches if object is a string starting with a given string. @param aString The string to search for. This value must not be @c nil. - + This matcher first checks whether the evaluated object is a string. If so, it checks if @a aString matches the beginning characters of the evaluated object. - + Example: - + @par @ref endsWith(@"foo") - + will match "foobar". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSubstringMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSubstringMatcher.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSubstringMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCSubstringMatcher.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h index b2e6333f56..b120ee71f7 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCWrapInMatcher.h @@ -12,9 +12,9 @@ /** Wraps argument in a matcher, if necessary. - + @return The argument as-if if it is already a matcher, otherwise wrapped in an @ref equalTo matcher. - + @ingroup helpers */ OBJC_EXPORT id HCWrapInMatcher(id matcherOrValue); diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h index 060d8c0738..50042f72bd 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/OCHamcrest.h @@ -108,7 +108,7 @@ @defgroup integration_numeric Unit Tests of Primitive Numbers Unit test integration for primitive numbers. - + The @c assertThat<Type> macros convert the primitive actual value to an @c NSNumber, passing that to the matcher for evaluation. If the matcher is not satisfied, an exception is thrown describing the mismatch. diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/OCHamcrest b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/OCHamcrest similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/OCHamcrest rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/OCHamcrest diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Resources/Info.plist b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Resources/Info.plist similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Resources/Info.plist rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/A/Resources/Info.plist diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/Current b/Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/Current similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/Current rename to Tests/Vendor/OCHamcrest/OCHamcrest.framework/Versions/Current diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Headers b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Headers similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Headers rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Headers diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/OCHamcrestIOS b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/OCHamcrestIOS similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/OCHamcrestIOS rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/OCHamcrestIOS diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Resources b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Resources similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Resources rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Resources diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h index 25637067b7..e0a717c359 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAllOf.h @@ -24,18 +24,18 @@ OBJC_EXPORT id HC_allOf(id match, ...) NS_REQUIRES_NIL_TERMINATION; /** allOf(firstMatcher, ...) - Matches if all of the given matchers evaluate to @c YES. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + The matchers are evaluated from left to right using short-circuit evaluation, so evaluation stops as soon as a matcher returns @c NO. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_allOf instead.) - + @ingroup logical_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h index b69ba31b3e..f0ae84b78d 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAnyOf.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_anyOf(id match, ...) NS_REQUIRES_NIL_TERMINATION; /** anyOf(firstMatcher, ...) - Matches if any of the given matchers evaluate to @c YES. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + The matchers are evaluated from left to right using short-circuit evaluation, so evaluation stops as soon as a matcher returns @c YES. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anyOf instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h index d699f8279b..ea50cab095 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCAssertThat.h @@ -19,20 +19,20 @@ OBJC_EXPORT void HC_assertThatWithLocation(id testCase, id actual, id /** assertThat(actual, matcher) - Asserts that actual value satisfies matcher. - + @param actual The object to evaluate as the actual value. @param matcher The matcher to satisfy as the expected condition. - + @c assertThat passes the actual value to the matcher for evaluation. If the matcher is not satisfied, an exception is thrown describing the mismatch. - + @c assertThat is designed to integrate well with OCUnit and other unit testing frameworks. Unmet assertions are reported as test failures. In Xcode, these failures can be clicked to reveal the line of the assertion. - + In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_assertThat instead. - + @ingroup integration */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h index 210045253e..298076c870 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseDescription.h @@ -11,7 +11,7 @@ /** Base class for all HCDescription implementations. - + @ingroup core */ @interface HCBaseDescription : NSObject diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseMatcher.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBaseMatcher.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBoxNumber.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBoxNumber.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBoxNumber.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCBoxNumber.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h index 334fe1a260..c1a9a038e1 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCCollectMatchers.h @@ -15,7 +15,7 @@ /** Returns an array of matchers from a variable-length comma-separated list terminated by @c nil. - + @ingroup helpers */ OBJC_EXPORT NSMutableArray *HCCollectMatchers(id item1, va_list args); diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h index 12fadbe7e0..eadee62530 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescribedAs.h @@ -31,16 +31,16 @@ OBJC_EXPORT id HC_describedAs(NSString *description, id ma /** describedAs(description, matcher, ...) - Adds custom failure description to a given matcher. - + @param description Overrides the matcher's description. @param matcher,... The matcher to satisfy, followed by a comma-separated list of substitution values ending with @c nil. - + The description may contain substitution placeholders \%0, \%1, etc. These will be replaced by any values that follow the matcher. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_describedAs instead.) - + @ingroup decorator_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h index 7b3d13212b..26296d38bf 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCDescription.h @@ -10,7 +10,7 @@ /** A description of an HCMatcher. - + An HCMatcher will describe itself to a description which can later be used for reporting. @ingroup core @@ -19,32 +19,32 @@ /** Appends some plain text to the description. - + @return @c self, for chaining. */ - (id)appendText:(NSString *)text; /** Appends description of given value to @c self. - + If the value implements the @ref HCSelfDescribing protocol, then it will be used. - + @return @c self, for chaining. */ - (id)appendDescriptionOf:(id)value; /** Appends an arbitary value to the description. - + @b Deprecated: Call @ref appendDescriptionOf: instead. @return @c self, for chaining. */ - (id)appendValue:(id)value __attribute__((deprecated)); -/** +/** Appends a list of objects to the description. - + @return @c self, for chaining. */ - (id)appendList:(NSArray *)values diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h index c9e8906a22..0dfc0aaa17 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasCount.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_hasCount(id matcher); Matches if object's @c -count satisfies a given matcher. @param aMatcher The matcher to satisfy. - + This matcher invokes @c -count on the evaluated object to get the number of elements it contains, passing the result to @a aMatcher for evaluation. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasCount instead.) @@ -47,10 +47,10 @@ OBJC_EXPORT id HC_hasCountOf(NSUInteger count); Matches if object's @c -count equals a given value. @param value @c NSUInteger value to compare against as the expected value. - + This matcher invokes @c -count on the evaluated object to get the number of elements it contains, comparing the result to @a value for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasCountOf instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h index a3c7d2060c..de453b3701 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasDescription.h @@ -23,11 +23,11 @@ OBJC_EXPORT id HC_hasDescription(id match); Matches if object's @c -description satisfies a given matcher. @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. - + This matcher invokes @c -description on the evaluated object to get its description, passing the result to a given matcher for evaluation. If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasDescription(@ref startsWith(\@"foo")) @li @ref hasDescription(\@"bar") diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h index aa90554bb5..a1cce28268 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCHasProperty.h @@ -25,23 +25,23 @@ OBJC_EXPORT id HC_hasProperty(NSString *name, id valueMatch); /** hasProperty(name, valueMatcher) - Matches if object has a method of a given name whose return value satisfies a given matcher. - + @param name The name of a method without arguments that returns an object. @param valueMatcher The matcher to satisfy for the return value, or an expected value for @ref equalTo matching. - + This matcher first checks if the evaluated object has a method with a name matching the given @c name. If so, it invokes the method and sees if the returned value satisfies @c valueMatcher. - + While this matcher is called "hasProperty", it's useful for checking the results of any simple methods, not just properties. - + Examples: @li @ref hasProperty(@"firstName", @"Joe") @li @ref hasProperty(@"firstName", startsWith(@"J")) - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasProperty instead.) - + @ingroup object_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h index 0ba4f3d80f..5158137fa6 100755 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h @@ -10,7 +10,7 @@ /** Supporting class for matching a feature of an object. - + Tests whether the result of passing a given invocation to the value satisfies a given matcher. @ingroup helpers @@ -24,7 +24,7 @@ /** Determines whether a mismatch will be described in short form. - + Default is long form, which describes the object, the name of the invocation, and the sub-matcher's mismatch diagnosis. Short form only has the sub-matcher's mismatch diagnosis. */ @@ -39,7 +39,7 @@ /** Helper method for creating an invocation. - + @b Deprecated: Use new name +invocationForSelector:onClass: */ + (NSInvocation *)createInvocationForSelector:(SEL)selector onClass:(Class)aClass __attribute__((deprecated)); diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h index 96ecc14740..9f93e9dc07 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIs.h @@ -28,7 +28,7 @@ OBJC_EXPORT id HC_is(id match); @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. This matcher compares the evaluated object to the given matcher. - + If the @a aMatcher argument is a matcher, its behavior is retained, but the test may be more expressive. For example: @li @ref assertThatInt(value, equalToInt(5)) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h similarity index 93% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h index b258e01a61..8d5eabe8a2 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsAnything.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_anything(); /** Matches anything. - - This matcher always evaluates to @c YES. Specify this in composite matchers when the value of a + + This matcher always evaluates to @c YES. Specify this in composite matchers when the value of a particular element is unimportant. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anything instead.) @@ -45,12 +45,12 @@ OBJC_EXPORT id HC_anythingWithDescription(NSString *aDescription); /** anythingWithDescription(description) - Matches anything. - + @param description A string used to describe this matcher. - - This matcher always evaluates to @c YES. Specify this in collection matchers when the value of a + + This matcher always evaluates to @c YES. Specify this in collection matchers when the value of a particular element in a collection is unimportant. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_anything instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h index 04edd81535..61ce97ce32 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCloseTo.h @@ -25,13 +25,13 @@ OBJC_EXPORT id HC_closeTo(double aValue, double aDelta); /** closeTo(aValue, aDelta) - Matches if object is a number close to a given value, within a given delta. - + @param aValue The @c double value to compare against as the expected value. @param aDelta The @c double maximum delta between the values for which the numbers are considered close. - + This matcher invokes @c -doubleValue on the evaluated object to get its value as a @c double. The result is compared against @a aValue to see if the difference is within a positive @a aDelta. - + Example: @li @ref closeTo(3.0, 0.25) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h index 8c781a7449..2ba3ef7266 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContaining.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_hasItem(id itemMatch); /** hasItem(aMatcher) - Matches if any element of collection satisfies a given matcher. - + @param aMatcher The matcher to satisfy, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated collection, searching for any element that satisfies a given matcher. If a matching element is found, @c hasItem is satisfied. - + If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasItem instead.) @@ -48,15 +48,15 @@ OBJC_EXPORT id HC_hasItems(id itemMatch, ...) NS_REQUIRES_NIL_TERMINA /** hasItems(firstMatcher, ...) - Matches if all of the given matchers are satisfied by any elements of the collection. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the given matchers, searching for any elements in the evaluated collection that satisfy them. If each matcher is satisfied, then @c hasItems is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c hasItems instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h similarity index 96% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h index 23fe21eb30..7be62ebb45 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInAnyOrder.h @@ -24,17 +24,17 @@ OBJC_EXPORT id HC_containsInAnyOrder(id itemMatch, ...) NS_REQUIRES_N /** containsInAnyOrder(firstMatcher, ...) - Matches if collection's elements, in any order, satisfy a given list of matchers. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection, seeing if each element satisfies any of the given matchers. The matchers are tried from left to right, and when a satisfied matcher is - found, it is no longer a candidate for the remaining elements. If a one-to-one correspondence is + found, it is no longer a candidate for the remaining elements. If a one-to-one correspondence is established between elements and matchers, @c containsInAnyOrder is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_containsInAnyOrder instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h index 8694ae5fba..bbe8ea3225 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionContainingInOrder.h @@ -24,15 +24,15 @@ OBJC_EXPORT id HC_contains(id itemMatch, ...) NS_REQUIRES_NIL_TERMINA /** contains(firstMatcher, ...) - Matches if collection's elements satisfy a given list of matchers, in order. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection and a given list of matchers, seeing if each element satisfies its corresponding matcher. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_contains instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h index 3c5c635a3a..5012143fb7 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsCollectionOnlyContaining.h @@ -24,22 +24,22 @@ OBJC_EXPORT id HC_onlyContains(id itemMatch, ...) NS_REQUIRES_NIL_TER /** onlyContains(firstMatcher, ...) - Matches if each element of collection satisfies any of the given matchers. - + @param firstMatcher,... A comma-separated list of matchers ending with @c nil. - + This matcher iterates the evaluated collection, confirming whether each element satisfies any of the given matchers. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. Example: - + @par @ref onlyContains(startsWith(@"Jo"), nil) - + will match a collection [@"Jon", @"John", @"Johann"]. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_onlyContains instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h index caa5e59760..3000fe36ab 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContaining.h @@ -28,16 +28,16 @@ OBJC_EXPORT id HC_hasEntry(id keyMatch, id valueMatch); /** hasEntry(keyMatcher, valueMatcher) - Matches if dictionary contains key-value entry satisfying a given pair of matchers. - + @param keyMatcher The matcher to satisfy for the key, or an expected value for @ref equalTo matching. @param valueMatcher The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry that satisfies @a keyMatcher and @a valueMatcher. If a matching entry is found, @c hasEntry is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasEntry(@ref equalTo(@"foo"), equalTo(@"bar")) @li @ref hasEntry(@"foo", @"bar") diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h index ee8e8e7002..f08ea5fe84 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingEntries.h @@ -29,10 +29,10 @@ OBJC_EXPORT id HC_hasEntries(id keysAndValueMatch, ...) NS_REQUIRES_N hasEntries(firstKey, valueMatcher, ...) - Matches if dictionary contains entries satisfying a list of alternating keys and their value matchers. - + @param firstKey A key (not a matcher) to look up. @param valueMatcher,... The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + Note that the keys must be actual keys, not matchers. Any value argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. The list must end with @c nil. @@ -40,7 +40,7 @@ OBJC_EXPORT id HC_hasEntries(id keysAndValueMatch, ...) NS_REQUIRES_N Examples: @li @ref hasEntries(@"first", equalTo(@"Jon"), @"last", equalTo(@"Reid"), nil) @li @ref hasEntries(@"first", @"Jon", @"last", @"Reid", nil) - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasEntry instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h index 24ffb82344..91e2edc303 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingKey.h @@ -24,22 +24,22 @@ OBJC_EXPORT id HC_hasKey(id keyMatch); /** hasKey(keyMatcher) - Matches if dictionary contains an entry whose key satisfies a given matcher. - + @param keyMatcher The matcher to satisfy for the key, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry whose key satisfies the given matcher. If a matching entry is found, @c hasKey is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasKey instead.) Examples: @li @ref hasEntry(equalTo(@"foo")) @li @ref hasEntry(@"foo") - + @ingroup collection_matchers */ #ifdef HC_SHORTHAND diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h index 5ddda591e6..4dcd75f8bd 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsDictionaryContainingValue.h @@ -24,19 +24,19 @@ OBJC_EXPORT id HC_hasValue(id valueMatch); /** hasValue(valueMatcher) - Matches if dictionary contains an entry whose value satisfies a given matcher. - + @param valueMatcher The matcher to satisfy for the value, or an expected value for @ref equalTo matching. - + This matcher iterates the evaluated dictionary, searching for any key-value entry whose value satisfies the given matcher. If a matching entry is found, @c hasValue is satisfied. - + Any argument that is not a matcher is implicitly wrapped in an @ref equalTo matcher to check for equality. - + Examples: @li @ref hasValue(equalTo(@"bar")) @li @ref hasValue(@"bar") - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_hasValue instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h index 20c024616c..8df8140bad 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEmptyCollection.h @@ -23,7 +23,7 @@ OBJC_EXPORT id HC_empty(); This matcher invokes @c -count on the evaluated object to determine if the number of elements it contains is zero. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_empty instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h index 18761c3ca6..67cc183872 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqual.h @@ -24,12 +24,12 @@ OBJC_EXPORT id HC_equalTo(id object); /** equalTo(anObject) - Matches if object is equal to a given object. - + @param anObject The object to compare against as the expected value. - + This matcher compares the evaluated object to @a anObject for equality, as determined by the @c -isEqual: method. - + If @a anObject is @c nil, the matcher will successfully match @c nil. (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h similarity index 95% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h index 5d974c7f86..724334e563 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringCase.h @@ -26,15 +26,15 @@ OBJC_EXPORT id HC_equalToIgnoringCase(NSString *aString); Matches if object is a string equal to a given string, ignoring case differences. @param aString The string to compare against as the expected value. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it compares it with + + This matcher first checks whether the evaluated object is a string. If so, it compares it with @a aString, ignoring differences of case. - + Example: - + @par @ref equalToIgnoringCase(@"hello world") - + will match "heLLo WorlD". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h similarity index 95% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h index 58e28d74dc..a2715c04cd 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualIgnoringWhiteSpace.h @@ -27,15 +27,15 @@ OBJC_EXPORT id HC_equalToIgnoringWhiteSpace(NSString *aString); Matches if object is a string equal to a given string, ignoring differences in whitespace. @param aString The string to compare against as the expected value. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it compares it with + + This matcher first checks whether the evaluated object is a string. If so, it compares it with @a aString, ignoring differences in runs of whitespace. - + Example: - + @par @ref equalToIgnoringWhiteSpace(@"hello world") - + will match @verbatim "hello world" @endverbatim (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h similarity index 91% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h index adeef61426..356e4a118e 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsEqualToNumber.h @@ -13,12 +13,12 @@ OBJC_EXPORT id HC_equalToBool(BOOL value); /** equalToBool(value) - Matches if object is equal to @c NSNumber created from a @c BOOL. - + @param value The @c BOOL value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c BOOL @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToBool instead.) @@ -34,12 +34,12 @@ OBJC_EXPORT id HC_equalToChar(char value); /** equalToChar(value) - Matches if object is equal to @c NSNumber created from a @c char. - + @param value The @c char value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c char @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToChar instead.) @@ -55,12 +55,12 @@ OBJC_EXPORT id HC_equalToDouble(double value); /** equalToDouble(value) - Matches if object is equal to @c NSNumber created from a @c double. - + @param value The @c double value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c double @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToDouble instead.) @@ -76,12 +76,12 @@ OBJC_EXPORT id HC_equalToFloat(float value); /** equalToFloat(value) - Matches if object is equal to @c NSNumber created from a @c float. - + @param value The @c float value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c float @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToFloat instead.) @@ -97,12 +97,12 @@ OBJC_EXPORT id HC_equalToInt(int value); /** equalToInt(value) - Matches if object is equal to @c NSNumber created from an @c int. - + @param value The @c int value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c int @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToInt instead.) @@ -118,12 +118,12 @@ OBJC_EXPORT id HC_equalToLong(long value); /** equalToLong(value) - Matches if object is equal to @c NSNumber created from a @c long. - + @param value The @c long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c long @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToLong instead.) @@ -139,12 +139,12 @@ OBJC_EXPORT id HC_equalToLongLong(long long value); /** equalToLongLong(value) - Matches if object is equal to @c NSNumber created from a long long. - + @param value The long long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a long long @a value and compares - the evaluated object to it for equality. - + the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToLongLong instead.) @@ -160,12 +160,12 @@ OBJC_EXPORT id HC_equalToShort(short value); /** equalToShort(value) - Matches if object is equal to @c NSNumber created from a @c short. - + @param value The @c short value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from a @c short @a value and compares the evaluated - object to it for equality. - + object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToShort instead.) @@ -181,12 +181,12 @@ OBJC_EXPORT id HC_equalToUnsignedChar(unsigned char value); /** equalToUnsignedChar(value) - Matches if object is equal to @c NSNumber created from an unsigned char. - + @param value The unsigned char value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned char @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedChar instead.) @@ -202,12 +202,12 @@ OBJC_EXPORT id HC_equalToUnsignedInt(unsigned int value); /** equalToUnsignedInt(value) - Matches if object is equal to @c NSNumber created from an unsigned int. - + @param value The unsigned int value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned int @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedInt instead.) @@ -223,12 +223,12 @@ OBJC_EXPORT id HC_equalToUnsignedLong(unsigned long value); /** equalToUnsignedLong(value) - Matches if object is equal to @c NSNumber created from an unsigned long. - + @param value The unsigned long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned long @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedLong instead.) @@ -244,12 +244,12 @@ OBJC_EXPORT id HC_equalToUnsignedLongLong(unsigned long long value); /** equalToUnsignedLongLong(value) - Matches if object is equal to @c NSNumber created from an unsigned long long. - + @param value The unsigned long long value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned long long @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedLongLong instead.) @@ -265,12 +265,12 @@ OBJC_EXPORT id HC_equalToUnsignedShort(unsigned short value); /** equalToUnsignedShort(value) - Matches if object is equal to @c NSNumber created from an unsigned short. - + @param value The unsigned short value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an unsigned short @a value and - compares the evaluated object to it for equality. - + compares the evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedShort instead.) @@ -286,12 +286,12 @@ OBJC_EXPORT id HC_equalToInteger(NSInteger value); /** equalToInteger(value) - Matches if object is equal to @c NSNumber created from an @c NSInteger. - + @param value The @c NSInteger value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an @c NSInteger @a value and compares the - evaluated object to it for equality. - + evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToInteger instead.) @@ -307,12 +307,12 @@ OBJC_EXPORT id HC_equalToUnsignedInteger(NSUInteger value); /** equalToUnsignedInteger(value) - Matches if object is equal to @c NSNumber created from an @c NSUInteger. - + @param value The @c NSUInteger value from which to create an @c NSNumber. - + This matcher creates an @c NSNumber object from an @c NSUInteger @a value and compares the - evaluated object to it for equality. - + evaluated object to it for equality. + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_equalToUnsignedInteger instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h index 1de4c761f2..653105358c 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsIn.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_isIn(id aCollection); Matches if evaluated object is present in a given collection. @param aCollection The collection to search. - + This matcher invokes @c -containsObject: on @a aCollection to determine if the evaluated object is an element of the collection. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_isIn instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h index 6c134921cd..6c11704219 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsInstanceOf.h @@ -24,12 +24,12 @@ OBJC_EXPORT id HC_instanceOf(Class aClass); /** instanceOf(aClass) - Matches if object is an instance of, or inherits from, a given class. - + @param aClass The class to compare against as the expected class. - + This matcher checks whether the evaluated object is an instance of @a aClass or an instance of any class that inherits from @a aClass. - + Example: @li @ref instanceOf([NSString class]) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h index d158516676..bc13159c6b 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNil.h @@ -34,7 +34,7 @@ OBJC_EXPORT id HC_notNilValue(); /** Matches if object is not @c nil. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_notNilValue instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h index 0c9dde45da..02078ae05d 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsNot.h @@ -26,7 +26,7 @@ OBJC_EXPORT id HC_isNot(id aMatcher); Inverts the given matcher to its logical negation. @param aMatcher The matcher to negate. - + This matcher compares the evaluated object to the negation of the given matcher. If the @a aMatcher argument is not a matcher, it is implicitly wrapped in an @ref equalTo matcher to check for equality, and thus matches for inequality. diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h index 007a93ce90..476f0ebe05 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCIsSame.h @@ -26,10 +26,10 @@ OBJC_EXPORT id HC_sameInstance(id object); Matches if evaluated object is the same instance as a given object. @param anObject The object to compare against as the expected value. - + This matcher compares the address of the evaluated object to determine if it is the same object as @a anObject. - + (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym @c HC_sameInstance instead.) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h index 4244216ec5..010c8c6d43 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCMatcher.h @@ -10,9 +10,9 @@ /** A matcher over acceptable values. - + A matcher is able to describe itself to give feedback when it fails. - + HCMatcher implementations should @b not directly implement this protocol. Instead, @b extend the HCBaseMatcher class, which will ensure that the HCMatcher API can grow to support new features and remain compatible with all HCMatcher implementations. @@ -40,12 +40,12 @@ /** Generates a description of why the matcher has not accepted the item. - + The description will be part of a larger description of why a matching failed, so it should be concise. - + This method assumes that @c matches:item is false, but will not check this. - + @param item The item that the HCMatcher has rejected. @param mismatchDescription The description to be built or appended to. */ diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCNumberAssert.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCNumberAssert.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCNumberAssert.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCNumberAssert.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h index 3cd919357a..e338649cb7 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCOrderingComparison.h @@ -34,9 +34,9 @@ OBJC_EXPORT id HC_greaterThan(id expected); /** greaterThan(aNumber) - Matches if object is greater than a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref greaterThan([NSNumber numberWithInt:5]) @@ -55,9 +55,9 @@ OBJC_EXPORT id HC_greaterThanOrEqualTo(id expected); /** greaterThanOrEqualTo(aNumber) - Matches if object is greater than or equal to a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref greaterThanOrEqualTo([NSNumber numberWithInt:5]) @@ -76,9 +76,9 @@ OBJC_EXPORT id HC_lessThan(id expected); /** lessThan(aNumber) - Matches if object is less than a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref lessThan([NSNumber numberWithInt:5]) @@ -97,9 +97,9 @@ OBJC_EXPORT id HC_lessThanOrEqualTo(id expected); /** lessThanOrEqualTo(aNumber) - Matches if object is less than or equal to a given number. - + @param aNumber The @c NSNumber to compare against. - + Example: @li @ref lessThanOrEqualTo([NSNumber numberWithInt:5]) diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h index 0643345183..2ec9b94258 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilObject.h @@ -11,7 +11,7 @@ /** Throws an NSException if @a obj is @c nil. - + @ingroup helpers */ OBJC_EXPORT void HCRequireNonNilObject(id obj); diff --git a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h index 5d887f1484..ee1aa723fc 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrest.framework/Versions/A/Headers/HCRequireNonNilString.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCRequireNonNilString.h @@ -11,9 +11,9 @@ /** Throws an NSException if @a string is @c nil. - + @b Deprecated: Use @ref HCRequireNonNilObject instead. - + @ingroup helpers */ OBJC_EXPORT void HCRequireNonNilString(NSString *string) __attribute__((deprecated)); diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h similarity index 98% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h index 0acb1bb662..31059edac8 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSelfDescribing.h @@ -19,10 +19,10 @@ /** Generates a description of the object. - + The description may be part of a description of a larger object of which this is just a component, so it should be worded appropriately. - + @param description The description to be built or appended to. */ - (void)describeTo:(id)description; diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h similarity index 94% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h index d98cebc46a..694ba22d63 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContains.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_containsString(NSString *aSubstring); Matches if object is a string containing a given string. @param aString The string to search for. This value must not be @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it checks whether it + + This matcher first checks whether the evaluated object is a string. If so, it checks whether it contains @a aString. - + Example: - + @par @ref containsString(@"def") - + will match "abcdefg". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h similarity index 93% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h index 736625ba5c..4a618cee1b 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringContainsInOrder.h @@ -26,11 +26,11 @@ OBJC_EXPORT id HC_stringContainsInOrder(NSString *substring, ...) NS_ Matches if object is a string containing a given list of substrings in relative order. @param firstString,... A comma-separated list of strings ending with @c nil. - - This matcher first checks whether the evaluated object is a string. If so, it checks whether it - contains a given list of strings, in relative order to each other. The searches are performed + + This matcher first checks whether the evaluated object is a string. If so, it checks whether it + contains a given list of strings, in relative order to each other. The searches are performed starting from the beginning of the evaluated string. - + Example: @par diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringDescription.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringDescription.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringDescription.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringDescription.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h index ddbfafd3b4..9d9292d6c4 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringEndsWith.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_endsWith(NSString *aSubstring); Matches if object is a string ending with a given string. @param aString The string to search for. This value must not be @c nil. - + This matcher first checks whether the evaluated object is a string. If so, it checks if @a aString matches the ending characters of the evaluated object. - + Example: - + @par @ref endsWith(@"bar") - + will match "foobar". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h index 4a02fd7af3..816bf1fcea 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCStringStartsWith.h @@ -22,15 +22,15 @@ OBJC_EXPORT id HC_startsWith(NSString *aSubstring); Matches if object is a string starting with a given string. @param aString The string to search for. This value must not be @c nil. - + This matcher first checks whether the evaluated object is a string. If so, it checks if @a aString matches the beginning characters of the evaluated object. - + Example: - + @par @ref endsWith(@"foo") - + will match "foobar". (In the event of a name clash, don't \#define @c HC_SHORTHAND and use the synonym diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSubstringMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSubstringMatcher.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSubstringMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCSubstringMatcher.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h similarity index 97% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h index b2e6333f56..b120ee71f7 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/HCWrapInMatcher.h @@ -12,9 +12,9 @@ /** Wraps argument in a matcher, if necessary. - + @return The argument as-if if it is already a matcher, otherwise wrapped in an @ref equalTo matcher. - + @ingroup helpers */ OBJC_EXPORT id HCWrapInMatcher(id matcherOrValue); diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/NSObject+HCSelfDescribingValue.h diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h similarity index 99% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h index 281306d6a4..358b2bc542 100644 --- a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h +++ b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Headers/OCHamcrestIOS.h @@ -108,7 +108,7 @@ @defgroup integration_numeric Unit Tests of Primitive Numbers Unit test integration for primitive numbers. - + The @c assertThat<Type> macros convert the primitive actual value to an @c NSNumber, passing that to the matcher for evaluation. If the matcher is not satisfied, an exception is thrown describing the mismatch. diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/OCHamcrestIOS b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/OCHamcrestIOS similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/OCHamcrestIOS rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/OCHamcrestIOS diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Resources/Info.plist b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Resources/Info.plist similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Resources/Info.plist rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/A/Resources/Info.plist diff --git a/Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/Current b/Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/Current similarity index 100% rename from Specs/Runner/OCHamcrest/OCHamcrestIOS.framework/Versions/Current rename to Tests/Vendor/OCHamcrest/OCHamcrestIOS.framework/Versions/Current diff --git a/Specs/Runner/OCMock/OCMock/NSInvocation+OCMAdditions.h b/Tests/Vendor/OCMock/OCMock/NSInvocation+OCMAdditions.h similarity index 100% rename from Specs/Runner/OCMock/OCMock/NSInvocation+OCMAdditions.h rename to Tests/Vendor/OCMock/OCMock/NSInvocation+OCMAdditions.h diff --git a/Specs/Runner/OCMock/OCMock/NSInvocation+OCMAdditions.m b/Tests/Vendor/OCMock/OCMock/NSInvocation+OCMAdditions.m similarity index 96% rename from Specs/Runner/OCMock/OCMock/NSInvocation+OCMAdditions.m rename to Tests/Vendor/OCMock/OCMock/NSInvocation+OCMAdditions.m index 2595523ad5..65947c29c8 100644 --- a/Specs/Runner/OCMock/OCMock/NSInvocation+OCMAdditions.m +++ b/Tests/Vendor/OCMock/OCMock/NSInvocation+OCMAdditions.m @@ -11,102 +11,102 @@ @implementation NSInvocation(OCMAdditions) - (id)getArgumentAtIndexAsObject:(int)argIndex { const char* argType; - + argType = [[self methodSignature] getArgumentTypeAtIndex:argIndex]; while(strchr("rnNoORV", argType[0]) != NULL) argType += 1; - + if((strlen(argType) > 1) && (strchr("{^", argType[0]) == NULL) && (strcmp("@?", argType) != 0)) [NSException raise:NSInvalidArgumentException format:@"Cannot handle argument type '%s'.", argType]; - - switch (argType[0]) + + switch (argType[0]) { case '#': - case '@': + case '@': { id value; [self getArgument:&value atIndex:argIndex]; return value; } case ':': - { - SEL s = (SEL)0; - [self getArgument:&s atIndex:argIndex]; - id value = NSStringFromSelector(s); - return value; - } - case 'i': + { + SEL s = (SEL)0; + [self getArgument:&s atIndex:argIndex]; + id value = NSStringFromSelector(s); + return value; + } + case 'i': { int value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithInt:value]; - } + } case 's': { short value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithShort:value]; - } + } case 'l': { long value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithLong:value]; - } + } case 'q': { long long value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithLongLong:value]; - } + } case 'c': { char value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithChar:value]; - } + } case 'C': { unsigned char value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithUnsignedChar:value]; - } + } case 'I': { unsigned int value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithUnsignedInt:value]; - } + } case 'S': { unsigned short value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithUnsignedShort:value]; - } + } case 'L': { unsigned long value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithUnsignedLong:value]; - } + } case 'Q': { unsigned long long value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithUnsignedLongLong:value]; - } + } case 'f': { float value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithFloat:value]; - } + } case 'd': { double value; [self getArgument:&value atIndex:argIndex]; return [NSNumber numberWithDouble:value]; - } + } case 'B': { bool value; @@ -125,8 +125,8 @@ - (id)getArgumentAtIndexAsObject:(int)argIndex NSMutableData *argumentData = [[[NSMutableData alloc] initWithLength:maxArgSize] autorelease]; [self getArgument:[argumentData mutableBytes] atIndex:argIndex]; return [NSValue valueWithBytes:[argumentData bytes] objCType:argType]; - } - + } + } [NSException raise:NSInvalidArgumentException format:@"Argument type '%s' not supported", argType]; return nil; @@ -136,10 +136,10 @@ - (NSString *)invocationDescription { NSMethodSignature *methodSignature = [self methodSignature]; NSUInteger numberOfArgs = [methodSignature numberOfArguments]; - + if (numberOfArgs == 2) return NSStringFromSelector([self selector]); - + NSArray *selectorParts = [NSStringFromSelector([self selector]) componentsSeparatedByString:@":"]; NSMutableString *description = [[NSMutableString alloc] init]; unsigned int i; @@ -148,7 +148,7 @@ - (NSString *)invocationDescription [description appendFormat:@"%@%@:", (i > 2 ? @" " : @""), [selectorParts objectAtIndex:(i - 2)]]; [description appendString:[self argumentDescriptionAtIndex:i]]; } - + return [description autorelease]; } @@ -180,14 +180,14 @@ - (NSString *)argumentDescriptionAtIndex:(int)argIndex case ':': return [self selectorDescriptionAtIndex:argIndex]; default: return [@""]; // avoid confusion with trigraphs... } - + } - (NSString *)objectDescriptionAtIndex:(int)anInt { id object; - + [self getArgument:&object atIndex:anInt]; if (object == nil) return @"nil"; @@ -201,9 +201,9 @@ - (NSString *)charDescriptionAtIndex:(int)anInt { unsigned char buffer[128]; memset(buffer, 0x0, 128); - + [self getArgument:&buffer atIndex:anInt]; - + // If there's only one character in the buffer, and it's 0 or 1, then we have a BOOL if (buffer[1] == '\0' && (buffer[0] == 0 || buffer[0] == 1)) return [NSString stringWithFormat:@"%@", (buffer[0] == 1 ? @"YES" : @"NO")]; @@ -215,7 +215,7 @@ - (NSString *)unsignedCharDescriptionAtIndex:(int)anInt { unsigned char buffer[128]; memset(buffer, 0x0, 128); - + [self getArgument:&buffer atIndex:anInt]; return [NSString stringWithFormat:@"'%c'", *buffer]; } @@ -223,7 +223,7 @@ - (NSString *)unsignedCharDescriptionAtIndex:(int)anInt - (NSString *)intDescriptionAtIndex:(int)anInt { int intValue; - + [self getArgument:&intValue atIndex:anInt]; return [NSString stringWithFormat:@"%d", intValue]; } @@ -231,7 +231,7 @@ - (NSString *)intDescriptionAtIndex:(int)anInt - (NSString *)unsignedIntDescriptionAtIndex:(int)anInt { unsigned int intValue; - + [self getArgument:&intValue atIndex:anInt]; return [NSString stringWithFormat:@"%d", intValue]; } @@ -239,7 +239,7 @@ - (NSString *)unsignedIntDescriptionAtIndex:(int)anInt - (NSString *)shortDescriptionAtIndex:(int)anInt { short shortValue; - + [self getArgument:&shortValue atIndex:anInt]; return [NSString stringWithFormat:@"%hi", shortValue]; } @@ -247,7 +247,7 @@ - (NSString *)shortDescriptionAtIndex:(int)anInt - (NSString *)unsignedShortDescriptionAtIndex:(int)anInt { unsigned short shortValue; - + [self getArgument:&shortValue atIndex:anInt]; return [NSString stringWithFormat:@"%hu", shortValue]; } @@ -255,7 +255,7 @@ - (NSString *)unsignedShortDescriptionAtIndex:(int)anInt - (NSString *)longDescriptionAtIndex:(int)anInt { long longValue; - + [self getArgument:&longValue atIndex:anInt]; return [NSString stringWithFormat:@"%d", longValue]; } @@ -263,7 +263,7 @@ - (NSString *)longDescriptionAtIndex:(int)anInt - (NSString *)unsignedLongDescriptionAtIndex:(int)anInt { unsigned long longValue; - + [self getArgument:&longValue atIndex:anInt]; return [NSString stringWithFormat:@"%u", longValue]; } @@ -271,7 +271,7 @@ - (NSString *)unsignedLongDescriptionAtIndex:(int)anInt - (NSString *)longLongDescriptionAtIndex:(int)anInt { long long longLongValue; - + [self getArgument:&longLongValue atIndex:anInt]; return [NSString stringWithFormat:@"%qi", longLongValue]; } @@ -279,7 +279,7 @@ - (NSString *)longLongDescriptionAtIndex:(int)anInt - (NSString *)unsignedLongLongDescriptionAtIndex:(int)anInt { unsigned long long longLongValue; - + [self getArgument:&longLongValue atIndex:anInt]; return [NSString stringWithFormat:@"%qu", longLongValue]; } @@ -287,7 +287,7 @@ - (NSString *)unsignedLongLongDescriptionAtIndex:(int)anInt - (NSString *)doubleDescriptionAtIndex:(int)anInt; { double doubleValue; - + [self getArgument:&doubleValue atIndex:anInt]; return [NSString stringWithFormat:@"%f", doubleValue]; } @@ -295,7 +295,7 @@ - (NSString *)doubleDescriptionAtIndex:(int)anInt; - (NSString *)floatDescriptionAtIndex:(int)anInt { float floatValue; - + [self getArgument:&floatValue atIndex:anInt]; return [NSString stringWithFormat:@"%f", floatValue]; } @@ -303,7 +303,7 @@ - (NSString *)floatDescriptionAtIndex:(int)anInt - (NSString *)structDescriptionAtIndex:(int)anInt; { void *buffer; - + [self getArgument:&buffer atIndex:anInt]; return [NSString stringWithFormat:@":(struct)%p", buffer]; } @@ -311,7 +311,7 @@ - (NSString *)structDescriptionAtIndex:(int)anInt; - (NSString *)pointerDescriptionAtIndex:(int)anInt { void *buffer; - + [self getArgument:&buffer atIndex:anInt]; return [NSString stringWithFormat:@"%p", buffer]; } @@ -319,9 +319,9 @@ - (NSString *)pointerDescriptionAtIndex:(int)anInt - (NSString *)cStringDescriptionAtIndex:(int)anInt { char buffer[128]; - + memset(buffer, 0x0, 128); - + [self getArgument:&buffer atIndex:anInt]; return [NSString stringWithFormat:@"\"%S\"", buffer]; } @@ -329,7 +329,7 @@ - (NSString *)cStringDescriptionAtIndex:(int)anInt - (NSString *)selectorDescriptionAtIndex:(int)anInt { SEL selectorValue; - + [self getArgument:&selectorValue atIndex:anInt]; return [NSString stringWithFormat:@"@selector(%@)", NSStringFromSelector(selectorValue)]; } diff --git a/Specs/Runner/OCMock/OCMock/NSMethodSignature+OCMAdditions.h b/Tests/Vendor/OCMock/OCMock/NSMethodSignature+OCMAdditions.h similarity index 100% rename from Specs/Runner/OCMock/OCMock/NSMethodSignature+OCMAdditions.h rename to Tests/Vendor/OCMock/OCMock/NSMethodSignature+OCMAdditions.h diff --git a/Specs/Runner/OCMock/OCMock/NSMethodSignature+OCMAdditions.m b/Tests/Vendor/OCMock/OCMock/NSMethodSignature+OCMAdditions.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/NSMethodSignature+OCMAdditions.m rename to Tests/Vendor/OCMock/OCMock/NSMethodSignature+OCMAdditions.m diff --git a/Specs/Runner/OCMock/OCMock/NSNotificationCenter+OCMAdditions.h b/Tests/Vendor/OCMock/OCMock/NSNotificationCenter+OCMAdditions.h similarity index 100% rename from Specs/Runner/OCMock/OCMock/NSNotificationCenter+OCMAdditions.h rename to Tests/Vendor/OCMock/OCMock/NSNotificationCenter+OCMAdditions.h diff --git a/Specs/Runner/OCMock/OCMock/NSNotificationCenter+OCMAdditions.m b/Tests/Vendor/OCMock/OCMock/NSNotificationCenter+OCMAdditions.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/NSNotificationCenter+OCMAdditions.m rename to Tests/Vendor/OCMock/OCMock/NSNotificationCenter+OCMAdditions.m diff --git a/Specs/Runner/OCMock/OCMock/OCClassMockObject.h b/Tests/Vendor/OCMock/OCMock/OCClassMockObject.h similarity index 89% rename from Specs/Runner/OCMock/OCMock/OCClassMockObject.h rename to Tests/Vendor/OCMock/OCMock/OCClassMockObject.h index 69945b2199..660f6f36b6 100644 --- a/Specs/Runner/OCMock/OCMock/OCClassMockObject.h +++ b/Tests/Vendor/OCMock/OCMock/OCClassMockObject.h @@ -5,7 +5,7 @@ #import -@interface OCClassMockObject : OCMockObject +@interface OCClassMockObject : OCMockObject { Class mockedClass; } diff --git a/Specs/Runner/OCMock/OCMock/OCClassMockObject.m b/Tests/Vendor/OCMock/OCMock/OCClassMockObject.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCClassMockObject.m rename to Tests/Vendor/OCMock/OCMock/OCClassMockObject.m diff --git a/Specs/Runner/OCMock/OCMock/OCMArg.h b/Tests/Vendor/OCMock/OCMock/OCMArg.h similarity index 96% rename from Specs/Runner/OCMock/OCMock/OCMArg.h rename to Tests/Vendor/OCMock/OCMock/OCMArg.h index 669c094878..886d5e6351 100644 --- a/Specs/Runner/OCMock/OCMock/OCMArg.h +++ b/Tests/Vendor/OCMock/OCMock/OCMArg.h @@ -5,7 +5,7 @@ #import -@interface OCMArg : NSObject +@interface OCMArg : NSObject // constraining arguments diff --git a/Specs/Runner/OCMock/OCMock/OCMArg.m b/Tests/Vendor/OCMock/OCMock/OCMArg.m similarity index 97% rename from Specs/Runner/OCMock/OCMock/OCMArg.m rename to Tests/Vendor/OCMock/OCMock/OCMArg.m index 6cf198f836..fd817d942d 100644 --- a/Specs/Runner/OCMock/OCMock/OCMArg.m +++ b/Tests/Vendor/OCMock/OCMock/OCMArg.m @@ -44,7 +44,7 @@ + (id)checkWithSelector:(SEL)selector onObject:(id)anObject #if NS_BLOCKS_AVAILABLE -+ (id)checkWithBlock:(BOOL (^)(id))block ++ (id)checkWithBlock:(BOOL (^)(id))block { return [[[OCMBlockConstraint alloc] initWithConstraintBlock:block] autorelease]; } diff --git a/Specs/Runner/OCMock/OCMock/OCMBlockCaller.h b/Tests/Vendor/OCMock/OCMock/OCMBlockCaller.h similarity index 92% rename from Specs/Runner/OCMock/OCMock/OCMBlockCaller.h rename to Tests/Vendor/OCMock/OCMock/OCMBlockCaller.h index 652acc3471..ff6e3e607a 100644 --- a/Specs/Runner/OCMock/OCMock/OCMBlockCaller.h +++ b/Tests/Vendor/OCMock/OCMock/OCMBlockCaller.h @@ -7,7 +7,7 @@ #if NS_BLOCKS_AVAILABLE -@interface OCMBlockCaller : NSObject +@interface OCMBlockCaller : NSObject { void (^block)(NSInvocation *); } diff --git a/Specs/Runner/OCMock/OCMock/OCMBlockCaller.m b/Tests/Vendor/OCMock/OCMock/OCMBlockCaller.m similarity index 88% rename from Specs/Runner/OCMock/OCMock/OCMBlockCaller.m rename to Tests/Vendor/OCMock/OCMock/OCMBlockCaller.m index 439d885d37..fd33fdd159 100644 --- a/Specs/Runner/OCMock/OCMock/OCMBlockCaller.m +++ b/Tests/Vendor/OCMock/OCMock/OCMBlockCaller.m @@ -9,14 +9,14 @@ @implementation OCMBlockCaller --(id)initWithCallBlock:(void (^)(NSInvocation *))theBlock +-(id)initWithCallBlock:(void (^)(NSInvocation *))theBlock { self = [super init]; block = [theBlock copy]; return self; } --(void)dealloc +-(void)dealloc { [block release]; [super dealloc]; diff --git a/Specs/Runner/OCMock/OCMock/OCMBoxedReturnValueProvider.h b/Tests/Vendor/OCMock/OCMock/OCMBoxedReturnValueProvider.h similarity index 82% rename from Specs/Runner/OCMock/OCMock/OCMBoxedReturnValueProvider.h rename to Tests/Vendor/OCMock/OCMock/OCMBoxedReturnValueProvider.h index f2d9c919d9..835302fe88 100644 --- a/Specs/Runner/OCMock/OCMock/OCMBoxedReturnValueProvider.h +++ b/Tests/Vendor/OCMock/OCMock/OCMBoxedReturnValueProvider.h @@ -5,7 +5,7 @@ #import "OCMReturnValueProvider.h" -@interface OCMBoxedReturnValueProvider : OCMReturnValueProvider +@interface OCMBoxedReturnValueProvider : OCMReturnValueProvider { } diff --git a/Specs/Runner/OCMock/OCMock/OCMBoxedReturnValueProvider.m b/Tests/Vendor/OCMock/OCMock/OCMBoxedReturnValueProvider.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMBoxedReturnValueProvider.m rename to Tests/Vendor/OCMock/OCMock/OCMBoxedReturnValueProvider.m diff --git a/Specs/Runner/OCMock/OCMock/OCMConstraint.h b/Tests/Vendor/OCMock/OCMock/OCMConstraint.h similarity index 97% rename from Specs/Runner/OCMock/OCMock/OCMConstraint.h rename to Tests/Vendor/OCMock/OCMock/OCMConstraint.h index 3ae1264603..19243601d9 100644 --- a/Specs/Runner/OCMock/OCMock/OCMConstraint.h +++ b/Tests/Vendor/OCMock/OCMock/OCMConstraint.h @@ -6,7 +6,7 @@ #import -@interface OCMConstraint : NSObject +@interface OCMConstraint : NSObject + (id)constraint; - (BOOL)evaluate:(id)value; diff --git a/Specs/Runner/OCMock/OCMock/OCMConstraint.m b/Tests/Vendor/OCMock/OCMock/OCMConstraint.m similarity index 98% rename from Specs/Runner/OCMock/OCMock/OCMConstraint.m rename to Tests/Vendor/OCMock/OCMock/OCMConstraint.m index e70e25981d..8da575b56e 100644 --- a/Specs/Runner/OCMock/OCMock/OCMConstraint.m +++ b/Tests/Vendor/OCMock/OCMock/OCMConstraint.m @@ -22,7 +22,7 @@ - (BOOL)evaluate:(id)value + (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject { OCMInvocationConstraint *constraint = [OCMInvocationConstraint constraint]; - NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; + NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; if(signature == nil) [NSException raise:NSInvalidArgumentException format:@"Unkown selector %@ used in constraint.", NSStringFromSelector(aSelector)]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; @@ -126,7 +126,7 @@ - (id)initWithConstraintBlock:(BOOL (^)(id))aBlock; return self; } -- (BOOL)evaluate:(id)value +- (BOOL)evaluate:(id)value { return block(value); } diff --git a/Specs/Runner/OCMock/OCMock/OCMExceptionReturnValueProvider.h b/Tests/Vendor/OCMock/OCMock/OCMExceptionReturnValueProvider.h similarity index 98% rename from Specs/Runner/OCMock/OCMock/OCMExceptionReturnValueProvider.h rename to Tests/Vendor/OCMock/OCMock/OCMExceptionReturnValueProvider.h index 8e97469d48..b3ff6d36d6 100644 --- a/Specs/Runner/OCMock/OCMock/OCMExceptionReturnValueProvider.h +++ b/Tests/Vendor/OCMock/OCMock/OCMExceptionReturnValueProvider.h @@ -5,7 +5,7 @@ #import "OCMReturnValueProvider.h" -@interface OCMExceptionReturnValueProvider : OCMReturnValueProvider +@interface OCMExceptionReturnValueProvider : OCMReturnValueProvider { } diff --git a/Specs/Runner/OCMock/OCMock/OCMExceptionReturnValueProvider.m b/Tests/Vendor/OCMock/OCMock/OCMExceptionReturnValueProvider.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMExceptionReturnValueProvider.m rename to Tests/Vendor/OCMock/OCMock/OCMExceptionReturnValueProvider.m diff --git a/Specs/Runner/OCMock/OCMock/OCMIndirectReturnValueProvider.h b/Tests/Vendor/OCMock/OCMock/OCMIndirectReturnValueProvider.h similarity index 89% rename from Specs/Runner/OCMock/OCMock/OCMIndirectReturnValueProvider.h rename to Tests/Vendor/OCMock/OCMock/OCMIndirectReturnValueProvider.h index 4efeacb1e9..c335ec2634 100644 --- a/Specs/Runner/OCMock/OCMock/OCMIndirectReturnValueProvider.h +++ b/Tests/Vendor/OCMock/OCMock/OCMIndirectReturnValueProvider.h @@ -5,7 +5,7 @@ #import -@interface OCMIndirectReturnValueProvider : NSObject +@interface OCMIndirectReturnValueProvider : NSObject { id provider; SEL selector; diff --git a/Specs/Runner/OCMock/OCMock/OCMIndirectReturnValueProvider.m b/Tests/Vendor/OCMock/OCMock/OCMIndirectReturnValueProvider.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMIndirectReturnValueProvider.m rename to Tests/Vendor/OCMock/OCMock/OCMIndirectReturnValueProvider.m diff --git a/Specs/Runner/OCMock/OCMock/OCMNotificationPoster.h b/Tests/Vendor/OCMock/OCMock/OCMNotificationPoster.h similarity index 90% rename from Specs/Runner/OCMock/OCMock/OCMNotificationPoster.h rename to Tests/Vendor/OCMock/OCMock/OCMNotificationPoster.h index 817da78034..88585eeacb 100644 --- a/Specs/Runner/OCMock/OCMock/OCMNotificationPoster.h +++ b/Tests/Vendor/OCMock/OCMock/OCMNotificationPoster.h @@ -5,7 +5,7 @@ #import -@interface OCMNotificationPoster : NSObject +@interface OCMNotificationPoster : NSObject { NSNotification *notification; } diff --git a/Specs/Runner/OCMock/OCMock/OCMNotificationPoster.m b/Tests/Vendor/OCMock/OCMock/OCMNotificationPoster.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMNotificationPoster.m rename to Tests/Vendor/OCMock/OCMock/OCMNotificationPoster.m diff --git a/Specs/Runner/OCMock/OCMock/OCMObserverRecorder.h b/Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.h similarity index 92% rename from Specs/Runner/OCMock/OCMock/OCMObserverRecorder.h rename to Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.h index 556da3c00e..49cdd02887 100644 --- a/Specs/Runner/OCMock/OCMock/OCMObserverRecorder.h +++ b/Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.h @@ -5,7 +5,7 @@ #import -@interface OCMObserverRecorder : NSObject +@interface OCMObserverRecorder : NSObject { NSNotification *recordedNotification; } diff --git a/Specs/Runner/OCMock/OCMock/OCMObserverRecorder.m b/Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.m similarity index 99% rename from Specs/Runner/OCMock/OCMock/OCMObserverRecorder.m rename to Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.m index e50be50c2a..024f1151c9 100644 --- a/Specs/Runner/OCMock/OCMock/OCMObserverRecorder.m +++ b/Tests/Vendor/OCMock/OCMock/OCMObserverRecorder.m @@ -51,7 +51,7 @@ - (BOOL)matchesNotification:(NSNotification *)aNotification - (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg { if([expectedArg isKindOfClass:[OCMConstraint class]]) - { + { if([expectedArg evaluate:observedArg] == NO) return NO; } diff --git a/Specs/Runner/OCMock/OCMock/OCMPassByRefSetter.h b/Tests/Vendor/OCMock/OCMock/OCMPassByRefSetter.h similarity index 89% rename from Specs/Runner/OCMock/OCMock/OCMPassByRefSetter.h rename to Tests/Vendor/OCMock/OCMock/OCMPassByRefSetter.h index 12b99e3521..aeb00421ea 100644 --- a/Specs/Runner/OCMock/OCMock/OCMPassByRefSetter.h +++ b/Tests/Vendor/OCMock/OCMock/OCMPassByRefSetter.h @@ -5,7 +5,7 @@ #import -@interface OCMPassByRefSetter : NSObject +@interface OCMPassByRefSetter : NSObject { id value; } diff --git a/Specs/Runner/OCMock/OCMock/OCMPassByRefSetter.m b/Tests/Vendor/OCMock/OCMock/OCMPassByRefSetter.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMPassByRefSetter.m rename to Tests/Vendor/OCMock/OCMock/OCMPassByRefSetter.m diff --git a/Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.h b/Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.h similarity index 88% rename from Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.h rename to Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.h index 9357a42244..641cb78997 100644 --- a/Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.h +++ b/Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.h @@ -5,7 +5,7 @@ #import -@interface OCMRealObjectForwarder : NSObject +@interface OCMRealObjectForwarder : NSObject { } diff --git a/Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.m b/Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.m similarity index 89% rename from Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.m rename to Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.m index 7c0a7db14e..ad541e7ec9 100644 --- a/Specs/Runner/OCMock/OCMock/OCMRealObjectForwarder.m +++ b/Tests/Vendor/OCMock/OCMock/OCMRealObjectForwarder.m @@ -10,18 +10,18 @@ @implementation OCMRealObjectForwarder -- (void)handleInvocation:(NSInvocation *)anInvocation +- (void)handleInvocation:(NSInvocation *)anInvocation { id invocationTarget = [anInvocation target]; SEL invocationSelector = [anInvocation selector]; SEL aliasedSelector = NSSelectorFromString([OCMRealMethodAliasPrefix stringByAppendingString:NSStringFromSelector(invocationSelector)]); - + [anInvocation setSelector:aliasedSelector]; - if([invocationTarget isProxy] && (class_getInstanceMethod([invocationTarget class], @selector(realObject)))) + if([invocationTarget isProxy] && (class_getInstanceMethod([invocationTarget class], @selector(realObject)))) { // the method has been invoked on the mock, we need to change the target to the real object [anInvocation setTarget:[(OCPartialMockObject *)invocationTarget realObject]]; - } + } [anInvocation invoke]; } diff --git a/Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.h b/Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.h similarity index 90% rename from Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.h rename to Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.h index 3566c6ddb2..9c6d4a450c 100644 --- a/Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.h +++ b/Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.h @@ -5,7 +5,7 @@ #import -@interface OCMReturnValueProvider : NSObject +@interface OCMReturnValueProvider : NSObject { id returnValue; } diff --git a/Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.m b/Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.m similarity index 95% rename from Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.m rename to Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.m index d98b59c995..0567ebd5a6 100644 --- a/Specs/Runner/OCMock/OCMock/OCMReturnValueProvider.m +++ b/Tests/Vendor/OCMock/OCMock/OCMReturnValueProvider.m @@ -27,7 +27,7 @@ - (void)handleInvocation:(NSInvocation *)anInvocation const char *returnType = [[anInvocation methodSignature] methodReturnTypeWithoutQualifiers]; if(strcmp(returnType, @encode(id)) != 0) @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected invocation with object return type. Did you mean to use andReturnValue: instead?" userInfo:nil]; - [anInvocation setReturnValue:&returnValue]; + [anInvocation setReturnValue:&returnValue]; } @end diff --git a/Specs/Runner/OCMock/OCMock/OCMock-Info.plist b/Tests/Vendor/OCMock/OCMock/OCMock-Info.plist similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMock-Info.plist rename to Tests/Vendor/OCMock/OCMock/OCMock-Info.plist diff --git a/Specs/Runner/OCMock/OCMock/OCMock-Prefix.pch b/Tests/Vendor/OCMock/OCMock/OCMock-Prefix.pch similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMock-Prefix.pch rename to Tests/Vendor/OCMock/OCMock/OCMock-Prefix.pch diff --git a/Specs/Runner/OCMock/OCMock/OCMock.h b/Tests/Vendor/OCMock/OCMock/OCMock.h similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMock.h rename to Tests/Vendor/OCMock/OCMock/OCMock.h diff --git a/Specs/Runner/OCMock/OCMock/OCMockObject.h b/Tests/Vendor/OCMock/OCMock/OCMockObject.h similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCMockObject.h rename to Tests/Vendor/OCMock/OCMock/OCMockObject.h diff --git a/Specs/Runner/OCMock/OCMock/OCMockObject.m b/Tests/Vendor/OCMock/OCMock/OCMockObject.m similarity index 95% rename from Specs/Runner/OCMock/OCMock/OCMockObject.m rename to Tests/Vendor/OCMock/OCMock/OCMockObject.m index 198c683fcd..97b8f8c2a4 100644 --- a/Specs/Runner/OCMock/OCMock/OCMockObject.m +++ b/Tests/Vendor/OCMock/OCMock/OCMockObject.m @@ -137,12 +137,12 @@ - (void)verify { if([expectations count] == 1) { - [NSException raise:NSInternalInconsistencyException format:@"%@: expected method was not invoked: %@", + [NSException raise:NSInternalInconsistencyException format:@"%@: expected method was not invoked: %@", [self description], [[expectations objectAtIndex:0] description]]; } if([expectations count] > 0) { - [NSException raise:NSInternalInconsistencyException format:@"%@ : %d expected methods were not invoked: %@", + [NSException raise:NSInternalInconsistencyException format:@"%@ : %d expected methods were not invoked: %@", [self description], [expectations count], [self _recorderDescriptions:YES]]; } if([exceptions count] > 0) @@ -165,21 +165,21 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation { OCMockRecorder *recorder = nil; unsigned int i; - + for(i = 0; i < [recorders count]; i++) { recorder = [recorders objectAtIndex:i]; if([recorder matchesInvocation:anInvocation]) break; } - + if(i == [recorders count]) return NO; - - if([rejections containsObject:recorder]) + + if([rejections containsObject:recorder]) { NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException reason: - [NSString stringWithFormat:@"%@: explicitly disallowed method invoked: %@", [self description], + [NSString stringWithFormat:@"%@: explicitly disallowed method invoked: %@", [self description], [anInvocation invocationDescription]] userInfo:nil]; [exceptions addObject:exception]; [exception raise]; @@ -189,16 +189,16 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation { if(expectationOrderMatters && ([expectations objectAtIndex:0] != recorder)) { - [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", [self description], [recorder description], [[expectations objectAtIndex:0] description]]; - + } [[recorder retain] autorelease]; [expectations removeObject:recorder]; [recorders removeObjectAtIndex:i]; } [[recorder invocationHandlers] makeObjectsPerformSelector:@selector(handleInvocation:) withObject:anInvocation]; - + return YES; } @@ -207,7 +207,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation if(isNice == NO) { NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException reason: - [NSString stringWithFormat:@"%@: unexpected method invoked: %@ %@", [self description], + [NSString stringWithFormat:@"%@: unexpected method invoked: %@ %@", [self description], [anInvocation invocationDescription], [self _recorderDescriptions:NO]] userInfo:nil]; [exceptions addObject:exception]; [exception raise]; @@ -226,13 +226,13 @@ - (id)getNewRecorder - (NSString *)_recorderDescriptions:(BOOL)onlyExpectations { NSMutableString *outputString = [NSMutableString string]; - + OCMockRecorder *currentObject; NSEnumerator *recorderEnumerator = [recorders objectEnumerator]; while((currentObject = [recorderEnumerator nextObject]) != nil) { NSString *prefix; - + if(onlyExpectations) { if(![expectations containsObject:currentObject]) @@ -248,7 +248,7 @@ - (NSString *)_recorderDescriptions:(BOOL)onlyExpectations } [outputString appendFormat:@"\n\t%@\t%@", prefix, [currentObject description]]; } - + return outputString; } diff --git a/Specs/Runner/OCMock/OCMock/OCMockRecorder.h b/Tests/Vendor/OCMock/OCMock/OCMockRecorder.h similarity index 91% rename from Specs/Runner/OCMock/OCMock/OCMockRecorder.h rename to Tests/Vendor/OCMock/OCMock/OCMockRecorder.h index b11a25387f..3fc21e8a03 100644 --- a/Specs/Runner/OCMock/OCMock/OCMockRecorder.h +++ b/Tests/Vendor/OCMock/OCMock/OCMockRecorder.h @@ -5,7 +5,7 @@ #import -@interface OCMockRecorder : NSProxy +@interface OCMockRecorder : NSProxy { id signatureResolver; NSInvocation *recordedInvocation; @@ -23,7 +23,7 @@ - (id)andPost:(NSNotification *)aNotification; - (id)andCall:(SEL)selector onObject:(id)anObject; #if NS_BLOCKS_AVAILABLE -- (id)andDo:(void (^)(NSInvocation *))block; +- (id)andDo:(void (^)(NSInvocation *))block; #endif - (id)andForwardToRealObject; diff --git a/Specs/Runner/OCMock/OCMock/OCMockRecorder.m b/Tests/Vendor/OCMock/OCMock/OCMockRecorder.m similarity index 97% rename from Specs/Runner/OCMock/OCMock/OCMockRecorder.m rename to Tests/Vendor/OCMock/OCMock/OCMockRecorder.m index 5cd63d2b4c..faf21904a2 100644 --- a/Specs/Runner/OCMock/OCMock/OCMockRecorder.m +++ b/Tests/Vendor/OCMock/OCMock/OCMockRecorder.m @@ -87,7 +87,7 @@ - (id)andCall:(SEL)selector onObject:(id)anObject #if NS_BLOCKS_AVAILABLE -- (id)andDo:(void (^)(NSInvocation *))aBlock +- (id)andDo:(void (^)(NSInvocation *))aBlock { [invocationHandlers addObject:[[[OCMBlockCaller alloc] initWithCallBlock:aBlock] autorelease]]; return self; @@ -133,10 +133,10 @@ - (BOOL)matchesInvocation:(NSInvocation *)anInvocation { id recordedArg, passedArg; int i, n; - + if([anInvocation selector] != [recordedInvocation selector]) return NO; - + n = (int)[[recordedInvocation methodSignature] numberOfArguments]; for(i = 2; i < n; i++) { @@ -149,12 +149,12 @@ - (BOOL)matchesInvocation:(NSInvocation *)anInvocation return NO; continue; } - + if([recordedArg isKindOfClass:[NSValue class]]) recordedArg = [OCMArg resolveSpecialValues:recordedArg]; - + if([recordedArg isKindOfClass:[OCMConstraint class]]) - { + { if([recordedArg evaluate:passedArg] == NO) return NO; } @@ -170,7 +170,7 @@ - (BOOL)matchesInvocation:(NSInvocation *)anInvocation } else { - if(([recordedArg class] == [NSNumber class]) && + if(([recordedArg class] == [NSNumber class]) && ([(NSNumber*)recordedArg compare:(NSNumber*)passedArg] != NSOrderedSame)) return NO; if(([recordedArg isEqual:passedArg] == NO) && diff --git a/Specs/Runner/OCMock/OCMock/OCObserverMockObject.h b/Tests/Vendor/OCMock/OCMock/OCObserverMockObject.h similarity index 92% rename from Specs/Runner/OCMock/OCMock/OCObserverMockObject.h rename to Tests/Vendor/OCMock/OCMock/OCObserverMockObject.h index 908ad2f32b..763829b306 100644 --- a/Specs/Runner/OCMock/OCMock/OCObserverMockObject.h +++ b/Tests/Vendor/OCMock/OCMock/OCObserverMockObject.h @@ -5,7 +5,7 @@ #import -@interface OCObserverMockObject : NSObject +@interface OCObserverMockObject : NSObject { BOOL expectationOrderMatters; NSMutableArray *recorders; diff --git a/Specs/Runner/OCMock/OCMock/OCObserverMockObject.m b/Tests/Vendor/OCMock/OCMock/OCObserverMockObject.m similarity index 91% rename from Specs/Runner/OCMock/OCMock/OCObserverMockObject.m rename to Tests/Vendor/OCMock/OCMock/OCObserverMockObject.m index 85098565ae..2eb3251157 100644 --- a/Specs/Runner/OCMock/OCMock/OCObserverMockObject.m +++ b/Tests/Vendor/OCMock/OCMock/OCObserverMockObject.m @@ -48,12 +48,12 @@ - (void)verify { if([recorders count] == 1) { - [NSException raise:NSInternalInconsistencyException format:@"%@: expected notification was not observed: %@", + [NSException raise:NSInternalInconsistencyException format:@"%@: expected notification was not observed: %@", [self description], [[recorders lastObject] description]]; } if([recorders count] > 0) { - [NSException raise:NSInternalInconsistencyException format:@"%@ : %d expected notifications were not observed.", + [NSException raise:NSInternalInconsistencyException format:@"%@ : %d expected notifications were not observed.", [self description], [recorders count]]; } } @@ -65,7 +65,7 @@ - (void)verify - (void)handleNotification:(NSNotification *)aNotification { NSUInteger i, limit; - + limit = expectationOrderMatters ? 1 : [recorders count]; for(i = 0; i < limit; i++) { @@ -75,7 +75,7 @@ - (void)handleNotification:(NSNotification *)aNotification return; } } - [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected notification observed: %@", [self description], + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected notification observed: %@", [self description], [aNotification description]]; } diff --git a/Specs/Runner/OCMock/OCMock/OCPartialMockObject.h b/Tests/Vendor/OCMock/OCMock/OCPartialMockObject.h similarity index 91% rename from Specs/Runner/OCMock/OCMock/OCPartialMockObject.h rename to Tests/Vendor/OCMock/OCMock/OCPartialMockObject.h index bd549ed2a7..8c2dc397a5 100644 --- a/Specs/Runner/OCMock/OCMock/OCPartialMockObject.h +++ b/Tests/Vendor/OCMock/OCMock/OCPartialMockObject.h @@ -5,7 +5,7 @@ #import "OCClassMockObject.h" -@interface OCPartialMockObject : OCClassMockObject +@interface OCPartialMockObject : OCClassMockObject { NSObject *realObject; } diff --git a/Specs/Runner/OCMock/OCMock/OCPartialMockObject.m b/Tests/Vendor/OCMock/OCMock/OCPartialMockObject.m similarity index 97% rename from Specs/Runner/OCMock/OCMock/OCPartialMockObject.m rename to Tests/Vendor/OCMock/OCMock/OCPartialMockObject.m index ebb889f8df..136cdb3a44 100644 --- a/Specs/Runner/OCMock/OCMock/OCPartialMockObject.m +++ b/Tests/Vendor/OCMock/OCMock/OCPartialMockObject.m @@ -10,7 +10,7 @@ @interface OCPartialMockObject (Private) - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation; -@end +@end NSString *OCMRealMethodAliasPrefix = @"ocmock_replaced_"; @@ -91,11 +91,11 @@ - (void)setupSubclassForObject:(id)anObject { Class realClass = [anObject class]; double timestamp = [NSDate timeIntervalSinceReferenceDate]; - const char *className = [[NSString stringWithFormat:@"%@-%p-%f", realClass, anObject, timestamp] cString]; + const char *className = [[NSString stringWithFormat:@"%@-%p-%f", realClass, anObject, timestamp] cString]; Class subclass = objc_allocateClassPair(realClass, className, 0); objc_registerClassPair(subclass); object_setClass(anObject, subclass); - + Method forwardInvocationMethod = class_getInstanceMethod([self class], @selector(forwardInvocationForRealObject:)); IMP forwardInvocationImp = method_getImplementation(forwardInvocationMethod); const char *forwardInvocationTypes = method_getTypeEncoding(forwardInvocationMethod); @@ -109,7 +109,7 @@ - (void)setupForwarderForSelector:(SEL)selector IMP originalImp = method_getImplementation(originalMethod); IMP forwarderImp = [subclass instanceMethodForSelector:@selector(aMethodThatMustNotExist)]; - class_addMethod(subclass, method_getName(originalMethod), forwarderImp, method_getTypeEncoding(originalMethod)); + class_addMethod(subclass, method_getName(originalMethod), forwarderImp, method_getTypeEncoding(originalMethod)); SEL aliasSelector = NSSelectorFromString([OCMRealMethodAliasPrefix stringByAppendingString:NSStringFromSelector(selector)]); class_addMethod(subclass, aliasSelector, originalImp, method_getTypeEncoding(originalMethod)); diff --git a/Specs/Runner/OCMock/OCMock/OCPartialMockRecorder.h b/Tests/Vendor/OCMock/OCMock/OCPartialMockRecorder.h similarity index 85% rename from Specs/Runner/OCMock/OCMock/OCPartialMockRecorder.h rename to Tests/Vendor/OCMock/OCMock/OCPartialMockRecorder.h index 95ce4e6310..7752f1d6c4 100644 --- a/Specs/Runner/OCMock/OCMock/OCPartialMockRecorder.h +++ b/Tests/Vendor/OCMock/OCMock/OCPartialMockRecorder.h @@ -5,7 +5,7 @@ #import "OCMockRecorder.h" -@interface OCPartialMockRecorder : OCMockRecorder +@interface OCPartialMockRecorder : OCMockRecorder { } diff --git a/Specs/Runner/OCMock/OCMock/OCPartialMockRecorder.m b/Tests/Vendor/OCMock/OCMock/OCPartialMockRecorder.m similarity index 100% rename from Specs/Runner/OCMock/OCMock/OCPartialMockRecorder.m rename to Tests/Vendor/OCMock/OCMock/OCPartialMockRecorder.m diff --git a/Specs/Runner/OCMock/OCMock/OCProtocolMockObject.h b/Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.h similarity index 88% rename from Specs/Runner/OCMock/OCMock/OCProtocolMockObject.h rename to Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.h index 88f32299a5..c781a72030 100644 --- a/Specs/Runner/OCMock/OCMock/OCProtocolMockObject.h +++ b/Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.h @@ -5,7 +5,7 @@ #import -@interface OCProtocolMockObject : OCMockObject +@interface OCProtocolMockObject : OCMockObject { Protocol *mockedProtocol; } @@ -13,4 +13,3 @@ - (id)initWithProtocol:(Protocol *)aProtocol; @end - diff --git a/Specs/Runner/OCMock/OCMock/OCProtocolMockObject.m b/Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.m similarity index 94% rename from Specs/Runner/OCMock/OCMock/OCProtocolMockObject.m rename to Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.m index fa4130b85c..6786299441 100644 --- a/Specs/Runner/OCMock/OCMock/OCProtocolMockObject.m +++ b/Tests/Vendor/OCMock/OCMock/OCProtocolMockObject.m @@ -31,11 +31,11 @@ - (NSString *)description - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { struct objc_method_description methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, YES, YES); - if(methodDescription.name == NULL) + if(methodDescription.name == NULL) { methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, NO, YES); } - if(methodDescription.name == NULL) + if(methodDescription.name == NULL) { return nil; } diff --git a/Tests/Vendor/OCMock/OCMock/en.lproj/InfoPlist.strings b/Tests/Vendor/OCMock/OCMock/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b92732c79e --- /dev/null +++ b/Tests/Vendor/OCMock/OCMock/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ 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/Tools/rkkeypath b/Tools/rkkeypath index 28960fb347..821f699831 100755 Binary files a/Tools/rkkeypath and b/Tools/rkkeypath differ diff --git a/VERSION b/VERSION index 8caff32511..571215736a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.4-dev +0.10.1 diff --git a/Vendor/XMLReader/LICENCE b/Vendor/XMLReader/LICENCE new file mode 100644 index 0000000000..e14c37141c --- /dev/null +++ b/Vendor/XMLReader/LICENCE @@ -0,0 +1,17 @@ +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. \ No newline at end of file diff --git a/Vendor/XMLReader/XMLReader.h b/Vendor/XMLReader/XMLReader.h new file mode 100755 index 0000000000..8cb500b246 --- /dev/null +++ b/Vendor/XMLReader/XMLReader.h @@ -0,0 +1,25 @@ +// +// XMLReader.h +// +// + +#import + +@interface XMLReader : NSObject +{ + NSMutableArray *dictionaryStack; + NSMutableString *textInProgress; + NSError **errorPointer; +} + ++ (NSDictionary *)dictionaryForPath:(NSString *)path error:(NSError **)errorPointer; ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data error:(NSError **)errorPointer; ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string error:(NSError **)errorPointer; + +@end + +@interface NSDictionary (XMLReaderNavigation) + +- (id)retrieveForPath:(NSString *)navPath; + +@end \ No newline at end of file diff --git a/Vendor/XMLReader/XMLReader.m b/Vendor/XMLReader/XMLReader.m new file mode 100644 index 0000000000..345bb01ae1 --- /dev/null +++ b/Vendor/XMLReader/XMLReader.m @@ -0,0 +1,254 @@ +// +// XMLReader.m +// + +#import "XMLReader.h" + +NSString *const kXMLReaderTextNodeKey = @"text"; +NSString *const kXMLReaderAttributePrefix = @""; + +@interface XMLReader (Internal) + +- (id)initWithError:(NSError **)error; +- (NSDictionary *)objectWithData:(NSData *)data; + +@end + +@implementation NSDictionary (XMLReaderNavigation) + +- (id)retrieveForPath:(NSString *)navPath +{ + // Split path on dots + NSArray *pathItems = [navPath componentsSeparatedByString:@"."]; + + // Enumerate through array + NSEnumerator *e = [pathItems objectEnumerator]; + NSString *path; + + // Set first branch from self + id branch = [self objectForKey:[e nextObject]]; + int count = 1; + + while ((path = [e nextObject])) + { + // Check if this branch is an NSArray + if([branch isKindOfClass:[NSArray class]]) + { + if ([path isEqualToString:@"last"]) + { + branch = [branch lastObject]; + } + else + { + if ([branch count] > [path intValue]) + { + branch = [branch objectAtIndex:[path intValue]]; + } + else + { + branch = nil; + } + } + } + else + { + // branch is assumed to be an NSDictionary + branch = [branch objectForKey:path]; + } + + count++; + } + + return branch; +} + +@end + +@implementation XMLReader + +#pragma mark - +#pragma mark Public methods + ++ (NSDictionary *)dictionaryForPath:(NSString *)path error:(NSError **)errorPointer +{ + NSString *fullpath = [[NSBundle bundleForClass:self] pathForResource:path ofType:@"xml"]; + NSData *data = [[NSFileManager defaultManager] contentsAtPath:fullpath]; + NSDictionary *rootDictionary = [XMLReader dictionaryForXMLData:data error:errorPointer]; + + return rootDictionary; +} + ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data error:(NSError **)error +{ + XMLReader *reader = [[XMLReader alloc] initWithError:error]; + NSDictionary *rootDictionary = [reader objectWithData:data]; + [reader release]; + + return rootDictionary; +} + ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string error:(NSError **)error +{ + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + + return [XMLReader dictionaryForXMLData:data error:error]; +} + +#pragma mark - +#pragma mark Parsing + +- (id)initWithError:(NSError **)error +{ + if ((self = [super init])) + { + errorPointer = error; + } + + return self; +} + +- (void)dealloc +{ + [dictionaryStack release]; + [textInProgress release]; + + [super dealloc]; +} + +- (NSDictionary *)objectWithData:(NSData *)data +{ + // Clear out any old data + [dictionaryStack release]; + [textInProgress release]; + + dictionaryStack = [[NSMutableArray alloc] init]; + textInProgress = [[NSMutableString alloc] init]; + + // Initialize the stack with a fresh dictionary + [dictionaryStack addObject:[NSMutableDictionary dictionary]]; + + // Parse the XML + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; + parser.delegate = self; + BOOL success = [parser parse]; + [parser release]; + + // Return the stack's root dictionary on success + if (success) + { + NSDictionary *resultDict = [dictionaryStack objectAtIndex:0]; + + return resultDict; + } + + return nil; +} + +#pragma mark - +#pragma mark NSXMLParserDelegate methods + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict +{ + // Get the dictionary for the current level in the stack + NSMutableDictionary *parentDict = [dictionaryStack lastObject]; + + // Create the child dictionary for the new element + NSMutableDictionary *childDict = [NSMutableDictionary dictionary]; + + // Initialize child dictionary with the attributes, prefixed with '@' + for (NSString *key in attributeDict) { + [childDict setValue:[attributeDict objectForKey:key] + forKey:[NSString stringWithFormat:@"%@%@", kXMLReaderAttributePrefix, key]]; + } + + // If there's already an item for this key, it means we need to create an array + id existingValue = [parentDict objectForKey:elementName]; + + if (existingValue) + { + NSMutableArray *array = nil; + + if ([existingValue isKindOfClass:[NSMutableArray class]]) + { + // The array exists, so use it + array = (NSMutableArray *) existingValue; + } + else + { + // Create an array if it doesn't exist + array = [NSMutableArray array]; + [array addObject:existingValue]; + + // Replace the child dictionary with an array of children dictionaries + [parentDict setObject:array forKey:elementName]; + } + + // Add the new child dictionary to the array + [array addObject:childDict]; + } + else + { + // No existing value, so update the dictionary + [parentDict setObject:childDict forKey:elementName]; + } + + // Update the stack + [dictionaryStack addObject:childDict]; +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName +{ + // Update the parent dict with text info + NSMutableDictionary *dictInProgress = [dictionaryStack lastObject]; + + // Pop the current dict + [dictionaryStack removeLastObject]; + + // Set the text property + if ([textInProgress length] > 0) + { + if ([dictInProgress count] > 0) + { + [dictInProgress setObject:textInProgress forKey:kXMLReaderTextNodeKey]; + } + else + { + // Given that there will only ever be a single value in this dictionary, let's replace the dictionary with a simple string. + NSMutableDictionary *parentDict = [dictionaryStack lastObject]; + id parentObject = [parentDict objectForKey:elementName]; + + // Parent is an Array + if ([parentObject isKindOfClass:[NSArray class]]) + { + [parentObject removeLastObject]; + [parentObject addObject:textInProgress]; + } + + // Parent is a Dictionary + else + { + [parentDict removeObjectForKey:elementName]; + [parentDict setObject:textInProgress forKey:elementName]; + } + } + + // Reset the text + [textInProgress release]; + textInProgress = [[NSMutableString alloc] init]; + } +} + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string +{ + // Build the text value + [textInProgress appendString:[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; +} + +- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError +{ + // Set the error pointer to the parser's error object + if (errorPointer) + *errorPointer = parseError; +} + +@end \ No newline at end of file diff --git a/Vendor/appledoc/Templates/docset/Contents/Resources/nodes-template.xml b/Vendor/appledoc/Templates/docset/Contents/Resources/nodes-template.xml index 6a2af27609..f6da701905 100644 --- a/Vendor/appledoc/Templates/docset/Contents/Resources/nodes-template.xml +++ b/Vendor/appledoc/Templates/docset/Contents/Resources/nodes-template.xml @@ -5,6 +5,16 @@ {{projectName}} {{indexFilename}} + {{#hasDocs}} + + {{strings/docset/docsTitle}} + {{indexFilename}} + + {{#docs}}{{>NodeRef}} + {{/docs}} + + + {{/hasDocs}} {{#hasClasses}} {{strings/docset/classesTitle}} @@ -39,6 +49,8 @@ + {{#docs}}{{>Node}} + {{/docs}} {{#classes}}{{>Node}} {{/classes}} {{#categories}}{{>Node}} diff --git a/Vendor/appledoc/Templates/docset/Contents/Resources/tokens-template.xml b/Vendor/appledoc/Templates/docset/Contents/Resources/tokens-template.xml old mode 100644 new mode 100755 index fafb2a7b46..cb65383f09 --- a/Vendor/appledoc/Templates/docset/Contents/Resources/tokens-template.xml +++ b/Vendor/appledoc/Templates/docset/Contents/Resources/tokens-template.xml @@ -48,7 +48,7 @@ Section RelatedTokens EndSection Section Abstract -{{#abstract}}{{>GBCommentComponentsList}}{{/abstract}} +{{#abstract}}{{>GBCommentComponentsList}}{{/abstract}} EndSection Section MethodDeclaration @@ -56,6 +56,6 @@ Section MethodDeclaration EndSection Section GBCommentComponentsList -{{#components}}{{&textValue}}{{/components}} +{{#components}}{{textValue}}{{/components}} EndSection diff --git a/Vendor/appledoc/Templates/html/css/styles.css b/Vendor/appledoc/Templates/html/css/styles.css old mode 100644 new mode 100755 index cd00d3aa1f..bf946c02af --- a/Vendor/appledoc/Templates/html/css/styles.css +++ b/Vendor/appledoc/Templates/html/css/styles.css @@ -28,12 +28,12 @@ li { margin-bottom: 10px; } -a { +a, a code { text-decoration: none; color: #36C; } -a:hover { +a:hover, a:hover code { text-decoration: underline; color: #36C; } @@ -47,6 +47,28 @@ h2 { padding-bottom: 2px; } +table { + margin-bottom: 4em; + border-collapse:collapse; + vertical-align: middle; +} + +td { + border: 1px solid #9BB3CD; + padding: .667em; + font-size: 100%; +} + +th { + border: 1px solid #9BB3CD; + padding: .3em .667em .3em .667em; + background: #93A5BB; + font-size: 103%; + font-weight: bold; + color: white; + text-align: left; +} + /* @group Common page elements */ #top_header { @@ -66,6 +88,7 @@ h2 { } #contents, #overview_contents { + -webkit-overflow-scrolling: touch; border-top: 1px solid #2B334F; position: absolute; top: 91px; @@ -386,6 +409,13 @@ li#jumpto_button select { height: 0.7em; } +.note { + border: 1px solid #5088C5; + background-color: white; + margin: 1.667em 0 1.75em 0; + padding: 0 .667em .083em .750em; +} + .warning { border: 1px solid #5088C5; background-color: #F0F3F7; @@ -423,12 +453,16 @@ li#jumpto_button select { } .section-specification table { + margin-bottom: 0em; border-top: 1px solid #d6e0e5; } .section-specification td { vertical-align: top; border-bottom: 1px solid #d6e0e5; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; padding: .6em; } @@ -531,5 +565,20 @@ span.tooltip:hover span.tooltip { color: #666; } +#tocContainer.hideInXcode { + display: none; + border: 0px solid black; +} + +#top_header.hideInXcode { + display: none; +} + +#contents.hideInXcode { + border: 0px solid black; + top: 0px; + left: 0px; +} + /* @end */ diff --git a/Vendor/appledoc/Templates/html/index-template.html b/Vendor/appledoc/Templates/html/index-template.html index 5bcb249312..e1d246b28d 100644 --- a/Vendor/appledoc/Templates/html/index-template.html +++ b/Vendor/appledoc/Templates/html/index-template.html @@ -39,6 +39,17 @@

{{page/title}}

{{/comment}} {{/indexDescription}} + + {{#hasDocs}} +
+

{{strings/indexPage/docsTitle}}

+ +
+ {{/hasDocs}} {{#hasClasses}}
diff --git a/Vendor/appledoc/Templates/html/object-template.html b/Vendor/appledoc/Templates/html/object-template.html index f13783ed5e..8158785fee 100644 --- a/Vendor/appledoc/Templates/html/object-template.html +++ b/Vendor/appledoc/Templates/html/object-template.html @@ -206,6 +206,14 @@

{{strings/objectMethods/instanceMethodsTit } window.onload = init; + + // If showing in Xcode, hide the TOC and Header + if (navigator.userAgent.match(/xcode/i)) { + document.getElementById("contents").className = "hideInXcode" + document.getElementById("tocContainer").className = "hideInXcode" + document.getElementById("top_header").className = "hideInXcode" + } + diff --git a/Vendor/appledoc/appledoc b/Vendor/appledoc/appledoc index c1ad414df2..1e85672a83 100755 Binary files a/Vendor/appledoc/appledoc and b/Vendor/appledoc/appledoc differ 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.h b/Vendor/iso8601parser/ISO8601DateFormatter.h new file mode 100644 index 0000000000..af4b57ceb9 --- /dev/null +++ b/Vendor/iso8601parser/ISO8601DateFormatter.h @@ -0,0 +1,79 @@ +/*ISO8601DateFormatter.h + * + *Created by Peter Hosey on 2009-04-11. + *Copyright 2009 Peter Hosey. All rights reserved. + */ + +#import + +/*This class converts dates to and from ISO 8601 strings. A good introduction to ISO 8601: + * + *Parsing can be done strictly, or not. When you parse loosely, leading whitespace is ignored, as is anything after the date. + *The loose parser will return an NSDate for this string: @" \t\r\n\f\t 2006-03-02!!!" + *Leading non-whitespace will not be ignored; the string will be rejected, and nil returned. See the README that came with this addition. + * + *The strict parser will only accept a string if the date is the entire string. The above string would be rejected immediately, solely on these grounds. + *Also, the loose parser provides some extensions that the strict parser doesn't. + *For example, the standard says for "-DDD" (an ordinal date in the implied year) that the logical representation (meaning, hierarchically) would be "--DDD", but because that extra hyphen is "superfluous", it was omitted. + *The loose parser will accept the extra hyphen; the strict parser will not. + *A full list of these extensions is in the README file. + */ + +/*The format to either expect or produce. + *Calendar format is YYYY-MM-DD. + *Ordinal format is YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01. + *Week format is YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07. + */ +enum { + ISO8601DateFormatCalendar, + ISO8601DateFormatOrdinal, + ISO8601DateFormatWeek, +}; +typedef NSUInteger ISO8601DateFormat; + +//The default separator for time values. Currently, this is ':'. +extern unichar ISO8601DefaultTimeSeparatorCharacter; + +@interface ISO8601DateFormatter: NSFormatter +{ + NSString *lastUsedFormatString; + NSDateFormatter *unparsingFormatter; + + NSCalendar *parsingCalendar, *unparsingCalendar; + + NSTimeZone *defaultTimeZone; + ISO8601DateFormat format; + unichar timeSeparator; + BOOL includeTime; + BOOL parsesStrictly; +} + +//Call this if you get a memory warning. ++ (void) purgeGlobalCaches; + +@property(nonatomic, retain) NSTimeZone *defaultTimeZone; + +#pragma mark Parsing + +//As a formatter, this object converts strings to dates. + +@property BOOL parsesStrictly; + +- (NSDateComponents *) dateComponentsFromString:(NSString *)string; +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange; + +- (NSDate *) dateFromString:(NSString *)string; +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone; +- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange; + +#pragma mark Unparsing + +@property ISO8601DateFormat format; +@property BOOL includeTime; +@property unichar timeSeparator; + +- (NSString *) stringFromDate:(NSDate *)date; +- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; + +@end diff --git a/Vendor/iso8601parser/ISO8601DateFormatter.m b/Vendor/iso8601parser/ISO8601DateFormatter.m new file mode 100644 index 0000000000..6a3b65a8e3 --- /dev/null +++ b/Vendor/iso8601parser/ISO8601DateFormatter.m @@ -0,0 +1,902 @@ +/*ISO8601DateFormatter.m + * + *Created by Peter Hosey on 2009-04-11. + *Copyright 2009 Peter Hosey. All rights reserved. + */ + +#import +#import "ISO8601DateFormatter.h" +#import "RKLog.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitSupport + +#ifndef DEFAULT_TIME_SEPARATOR +# define DEFAULT_TIME_SEPARATOR ':' +#endif +unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR; + +//Unicode date formats. +#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd" +//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1. +#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD" +#define ISO_TIME_FORMAT @"HH:mm:ss" +#define ISO_TIME_WITH_TIMEZONE_FORMAT ISO_TIME_FORMAT @"Z" +//printf formats. +#define ISO_TIMEZONE_UTC_FORMAT @"Z" +#define ISO_TIMEZONE_OFFSET_FORMAT @"%+.2d%.2d" + +@interface ISO8601DateFormatter(UnparsingPrivate) + +- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep; + +- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone; +- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone; + +@end + +static NSMutableDictionary *timeZonesByOffset; + +@implementation ISO8601DateFormatter + ++ (void) initialize { + if (!timeZonesByOffset) { + timeZonesByOffset = [[NSMutableDictionary alloc] init]; + } +} + ++ (void) purgeGlobalCaches { + NSMutableDictionary *oldCache = timeZonesByOffset; + timeZonesByOffset = nil; + [oldCache release]; +} + +- (NSCalendar *) makeCalendarWithDesiredConfiguration { + NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; + calendar.firstWeekday = 2; //Monday + calendar.timeZone = [NSTimeZone defaultTimeZone]; + return calendar; +} + +- (id) init { + if ((self = [super init])) { + parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; + unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain]; + + format = ISO8601DateFormatCalendar; + timeSeparator = ISO8601DefaultTimeSeparatorCharacter; + includeTime = NO; + parsesStrictly = NO; + } + return self; +} +- (void) dealloc { + [defaultTimeZone release]; + + [unparsingFormatter release]; + [lastUsedFormatString release]; + [parsingCalendar release]; + [unparsingCalendar release]; + + [super dealloc]; +} + +@synthesize defaultTimeZone; +- (void) setDefaultTimeZone:(NSTimeZone *)tz { + if (defaultTimeZone != tz) { + [defaultTimeZone release]; + defaultTimeZone = [tz retain]; + + unparsingCalendar.timeZone = defaultTimeZone; + } +} + +//The following properties are only here because GCC doesn't like @synthesize in category implementations. + +#pragma mark Parsing + +@synthesize parsesStrictly; + +static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits); +static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits); +static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next); +static double read_double(const unsigned char *str, const unsigned char **next); +static BOOL is_leap_year(NSUInteger year); + +/*Valid ISO 8601 date formats: + * + *YYYYMMDD + *YYYY-MM-DD + *YYYY-MM + *YYYY + *YY //century + * //Implied century: YY is 00-99 + * YYMMDD + * YY-MM-DD + * -YYMM + * -YY-MM + * -YY + * //Implied year + * --MMDD + * --MM-DD + * --MM + * //Implied year and month + * ---DD + * //Ordinal dates: DDD is the number of the day in the year (1-366) + *YYYYDDD + *YYYY-DDD + * YYDDD + * YY-DDD + * -DDD + * //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week + *yyyyWwwd + *yyyy-Www-d + *yyyyWww + *yyyy-Www + *yyWwwd + *yy-Www-d + *yyWww + *yy-Www + * //Year of the implied decade + *-yWwwd + *-y-Www-d + *-yWww + *-y-Www + * //Week and day of implied year + * -Wwwd + * -Www-d + * //Week only of implied year + * -Www + * //Day only of implied week + * -W-d + */ + +- (NSDateComponents *) dateComponentsFromString:(NSString *)string { + return [self dateComponentsFromString:string timeZone:NULL]; +} +- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone { + return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL]; +} +- (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; + NSTimeInterval + 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)) { + range.location = NSNotFound; + isValidDate = NO; + } else { + //Skip leading whitespace. + NSUInteger i = 0U; + for(NSUInteger len = strlen((const char *)ch); i < len; ++i) { + 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; + } else { + while(*ch == '-') { + ++num_leading_hyphens; + ++ch; + } + + segment = read_segment(ch, &ch, &num_digits); + switch(num_digits) { + case 0: + if (*ch == 'W') { + if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) { + year = nowComponents.year; + month_or_week = 1U; + ch += 2; + goto parseDayAfterWeek; + } else if (num_leading_hyphens == 1U) { + year = nowComponents.year; + goto parseWeekAndDay; + } else + isValidDate = NO; + } else + isValidDate = NO; + break; + + case 8: //YYYY MM DD + if (num_leading_hyphens > 0U) + isValidDate = NO; + else { + day = segment % 100U; + segment /= 100U; + month_or_week = segment % 100U; + year = segment / 100U; + } + break; + + case 6: //YYMMDD (implicit century) + if (num_leading_hyphens > 0U) + isValidDate = NO; + else { + day = segment % 100U; + segment /= 100U; + month_or_week = segment % 100U; + year = nowComponents.year; + year -= (year % 100U); + 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; + else + month_or_week = day = 1U; + } else { + segment = read_segment(ch, &ch, &num_digits); + switch(num_digits) { + case 4: //MMDD + 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. + if (num_leading_hyphens == 1U) { + if (*ch == '-') ++ch; + if (*++ch == 'W') { + year = nowComponents.year; + year -= (year % 10U); + year += segment; + goto parseWeekAndDay; + } else + isValidDate = NO; + } else + isValidDate = NO; + break; + } + case 2: + switch(num_leading_hyphens) { + case 0: + if (*ch == '-') { + //Implicit century + year = nowComponents.year; + year -= (year % 100U); + year += segment; + + if (*++ch == 'W') + goto parseWeekAndDay; + else if (!isdigit(*ch)) { + goto centuryOnly; + } else { + //Get month and/or date. + segment = read_segment_4digits(ch, &ch, &num_digits); + 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; + break; + } + case 2: //YY-MM; YY-MM-DD + month_or_week = segment; + if (*ch == '-') { + if (isdigit(*++ch)) + day = read_segment_2digits(ch, &ch); + else + day = 1U; + } else + day = 1U; + break; + + case 3: //Ordinal date. + day = segment; + dateSpecification = dateOnly; + break; + } + } + } else if (*ch == 'W') { + 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'. + if (strict) + isValidDate = NO; + else + month_or_week = day = 1U; + } else { + month_or_week = read_segment_2digits(ch, &ch); + if (*ch == '-') ++ch; + parseDayAfterWeek: + day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U; + dateSpecification = week; + } + } else { + //Century only. Assume current year. + centuryOnly: + year = segment * 100U + nowComponents.year % 100U; + month_or_week = day = 1U; + } + break; + + case 1:; //-YY; -YY-MM (implicit century) + 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); + 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; + if (*ch == '-') { + ++ch; + 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; + else { + day = segment % 1000U; + year = segment / 1000U; + dateSpecification = dateOnly; + if (strict && (day > (365U + is_leap_year(year)))) + 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)) + isValidDate = NO; + else { + day = segment; + year = nowComponents.year; + dateSpecification = dateOnly; + if (strict && (day > (365U + is_leap_year(year)))) + 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) { + ++ch; + if ((timeSep == ',') || (timeSep == '.')) { + //We can't do fractional minutes when '.' is the segment separator. + //Only allow whole minutes and whole seconds. + minute = read_segment_2digits(ch, &ch); + if (*ch == timeSep) { + ++ch; + second = read_segment_2digits(ch, &ch); + } + } else { + //Allow a fractional minute. + //If we don't get a fraction, look for a seconds segment. + //Otherwise, the fraction of a minute is the seconds. + minute = read_double(ch, &ch); + second = modf(minute, &minute); + if (second > DBL_EPSILON) + second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30). + else if (*ch == timeSep) { + ++ch; + second = read_double(ch, &ch); + } + } + } + + if (!strict) { + if (isspace(*ch)) ++ch; + } + + switch(*ch) { + case 'Z': + timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + break; + + case '+': + case '-':; + BOOL negative = (*ch == '-'); + if (isdigit(*++ch)) { + //Read hour offset. + segment = *ch - '0'; + if (isdigit(*++ch)) { + segment *= 10U; + segment += *(ch++) - '0'; + } + 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'; + if (isdigit(*++ch)) { + segment *= 10U; + segment += *ch - '0'; + } + 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]; + if (!timeZone) { + timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset]; + if (timeZone) + [timeZonesByOffset setObject:timeZone forKey:offsetNum]; + } + } + } + } + } + + 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. + NSUInteger prevYear = year - 1U; + NSUInteger YY = prevYear % 100U; + NSUInteger C = prevYear - YY; + NSUInteger G = YY + YY / 4U; + NSUInteger isLeapYear = (((C / 100U) % 4U) * 5U); + 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; +} + +- (NSDate *) dateFromString:(NSString *)string { + return [self dateFromString:string timeZone:NULL]; +} +- (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 { + NSTimeZone *timeZone = nil; + NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange]; + if (outTimeZone) + *outTimeZone = timeZone; + parsingCalendar.timeZone = timeZone; + + RKLogDebug(@"TIMEZONE: %@", timeZone); + + return [parsingCalendar dateFromComponents:components]; +} + +- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error { + NSDate *date = [self dateFromString:string]; + if (outValue) + *outValue = date; + return (date != nil); +} + +#pragma mark Unparsing + +@synthesize format; +@synthesize includeTime; +@synthesize timeSeparator; + +- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep { + if (timeSep != ':') { + NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease]; + [timeFormatMutable replaceOccurrencesOfString:@":" + withString:[NSString stringWithCharacters:&timeSep length:1U] + options:NSBackwardsSearch | NSLiteralSearch + range:(NSRange){ 0UL, [timeFormat length] }]; + timeFormat = timeFormatMutable; + } + return timeFormat; +} + +- (NSString *) stringFromDate:(NSDate *)date { + NSTimeZone *timeZone = self.defaultTimeZone; + if (!timeZone) timeZone = [NSTimeZone defaultTimeZone]; + return [self stringFromDate:date timeZone:timeZone]; +} + +- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone { + switch (self.format) { + case ISO8601DateFormatCalendar: + return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone]; + case ISO8601DateFormatWeek: + return [self weekDateStringForDate:date timeZone:timeZone]; + case ISO8601DateFormatOrdinal: + return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone]; + default: + [NSException raise:NSInternalInconsistencyException format:@"self.format was %d, not calendar (%d), week (%d), or ordinal (%d)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal]; + return nil; + } +} + +- (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 + if (offset == 0) + str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT]; + 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]; +} + +/*Adapted from: + * Algorithm for Converting Gregorian Dates to ISO 8601 Week Date + * Rick McCarty, 1999 + * http://personal.ecu.edu/mccartyr/ISOwdALG.txt + */ +- (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 + }; + enum { + january = 1, february, march, + april, may, june, + 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; + } else { + NSInteger lengthOfYear = 365 + yearIsLeapYear; + if((lengthOfYear - dayOfYear) < (thursday - weekday)) { + ++year; + week = 1; + } else { + NSInteger J = dayOfYear + (sunday - weekday) + Jan1Weekday; + 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]; +} + +@end + +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 % 100U) != 0U) + || ((year % 400U) == 0U)); +} diff --git a/Vendor/iso8601parser/LICENSE.txt b/Vendor/iso8601parser/LICENSE.txt new file mode 100644 index 0000000000..8af4c5dc2a --- /dev/null +++ b/Vendor/iso8601parser/LICENSE.txt @@ -0,0 +1,9 @@ +Copyright © 2006–2011 Peter Hosey + All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +Neither the name of Peter Hosey nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Vendor/iso8601parser/README.txt b/Vendor/iso8601parser/README.txt new file mode 100644 index 0000000000..a14cbd74c4 --- /dev/null +++ b/Vendor/iso8601parser/README.txt @@ -0,0 +1,126 @@ +How to use in your program +========================== + +Add the source files to your project. + +Parsing +------- + +Create an ISO 8601 date formatter, then call [formatter dateFromString:myString]. The method will return either an NSDate or nil. + +There are a total of six parser methods. The one that contains the actual parser is -[ISO8601DateFormatter dateComponentsFromString:timeZone:range:]. The other five are based on this one. + +The "outTimeZone" parameter, when not set to NULL, is a pointer to an NSTimeZone *variable. If the string specified a time zone, you'll receive the time zone object in that variable. If the string didn't specify a time zone, you'll receive nil. + +The "outRange" parameter, when not set to NULL, is a pointer to NSRange storage. You will receive the range of the parsed substring in that storage. + +Unparsing +--------- + +Create an ISO 8601 date formatter, then call [formatter stringFromDate:myDate]. The method will return a string. + +The formatter has several properties that control its behavior: + +* You can set the format of the resulting strings. By default, the formatter will generate calendar-date strings; your other options are week dates and ordinal dates. +* You can set a default time zone; by default, it will use [NSTimeZone defaultTimeZone]. +* You can enable a strict mode, wherein the formatter enforces sanity checks on the string. By default, the parser will afford you quite a bit of leeway. +* You can set whether to include the time in the string, and if so, what hour-minute separator to use (default ':'). + +How to test that this code works +================================ + +'make test' will perform all tests. If you want to perform only *some* tests: + +Parsing +------- + +Type 'make parser-test'. make will build the test program (testparser), then invoke testparser.sh.py to generate testparser.sh. Then make will invoke testparser.sh, which will invoke the test program with various dates. + +If you don't want to use my tests, 'make testparser' will create the test program without running it. You can then invoke testparser yourself with any date you want to. If it doesn't give you the result you expected, contact me, making sure to provide me with both the input and the output. + +Unparsing +--------- + +Type 'make unparser-test'. make will build the test programs, then invoke testunparser.sh. This shell script invokes each test program for -01-01 of every year from 1991 to 2010, writing the output to a file, and then runs diff -qs between that file (testunparser.out) and a file (testunparser-expected.out) containing known correct output. diff should report that the files are identical. + +Three test programs are included: unparse-date, unparse-weekdate, and unparse-ordinal date. If you don't want to use my tests, you can make these test programs separately. Each takes a date specified by ISO 8601 (parsed with my own ISO 8601 parser), and outputs a string that should represent the same date. + +Notes +===== + +Version history +--------------- + +This version is 0.6. Changes from 0.5: +* When not set to strict parsing, allow a space before the time-zone specification, for greater compatibility with NSDate output (`description`) and input (`dateWithString:`). 27603efc8a77 +* Bug fix: We no longer try to format the formatter. 83415de9f527 +* Bug fix: Hours are now zero-padded correctly ([#4](https://bitbucket.org/boredzo/iso-8601-parser-unparser/issue/4/time-zones-are-emitted-without-leading)). a5608e189ebe af0c6b397428 +* Fixed many various compiler warnings. Some fixes contributed by Sparks. 8be3d6f7c6f2 78ae31de2170 68dc351e9fdb e7ea87db8621 873f499ae6db +* Now 75 times faster. 6a023812bd2b 05dc35d6b505 3b2225e0ce8c d59720ad015a 10f526956963 297b8dae4095 796be11aa596 cadf0f0c8199 61d2959c6921 +* iOS users can now tell the ISO 8601 date formatter class to drop its caches (which you should do when you receive a memory warning). 2bb1725914b1 +* Added a couple of command-line options to the calendar-format-unparser test tool. c644aadb2b14 +* The parser test tool now displays parsed dates in GMT rather than the local time zone. 788c1455ecb1 +* Added a test tool to test unparsing with the time included. a89a9a8b3d61 +* You can now “make analysis” to run the Clang Static Analyzer on the date formatter. b3dd33841f42 +* The test tools are now built using Clang. 0723d3aa6596 + +Changes in 0.5 from 0.4: +* Rewrote as an NSFormatter subclass using NSCalendar. + * Making it a formatter makes it much easier to use with Bindings. + * Using NSCalendar means we're no longer using NSCalendarDate, which Apple has said they will deprecate at some point. +* Fixed a bug in week date generation: One subtraction could give a negative result, which was a problem because my implementation of the algorithm used unsigned integers. I've changed it to use signed integers, so the result truly is negative now. I also added a test case for this. + +Changes in 0.4 from 0.3: +* Added the ability to use a time separator other than ':'. + +Changes in 0.3 from 0.2: +* Colin Barrett noticed that I used %m instead of %M when creating the time strings. Oops. +* Colin also noticed that I had the ?: in -ISO8601DateStringWithTime: the wrong way around. Oops again. + +Changes in 0.2 from 0.1: +* The unparser is new. The has been munged to allow both components together, +* The parser has not changed. + +Parsing +------- + +Whitespace before a date, and anything after a date, is ignored. Thus, " T23 and all's well" is a valid date for the purpose of this method. (Yes, T23 is a valid ISO 8601 date. It means 23:00:00, or 11 PM.) + +All of the frills of ISO 8601 are supported, except for extended dates (years longer than 4 digits). Specifically, you can use week-based dates (2006-W2 for the second week of 2006), ordinal dates (2006-365 for December 31), decimal minutes (11:30.5 == 11:30:30), and decimal seconds (11:30:10.5). All methods of specifying a time zone are supported. + +ISO 8601 leaves quite a bit up to the parties exchanging dates. I hope I've chosen reasonable defaults. For example (note that I'm writing this on 2006-02-24): + +• If the month or month and date are missing, 1 is assumed. "2006" == "2006-01-01". +• If the year or year and month are missing, the current ones are assumed. "--02-01" == "2006-02-01". "---28" == "2006-02-28". +• In the case of week-based dates, with the day missing, this implementation returns the first day of that week: 2006-W1 is 2006-01-01, 2006-W2 is 2006-01-08, etc. +• For any date without a time, midnight on that date is used. +• ISO 8601 permits the choice of either T0 or T24 for midnight. This implementation uses T0. T24 will get you T0 on the following day. +• If no time-zone is specified, local time (as returned by [NSTimeZone localTimeZone]) is used. + +When a date is parsed that has a year but no century, this implementation adds the current century. + +The implementation is tolerant of out-of-range numbers. For example, "2005-13-40T24:62:89" == 1:02 AM on 2006-02-10. Notice that the month (13 > 12), date (40 > 31), hour (24 > 23), minute (62 > 59), and second (89 > 59) are all out-of-range. + +As mentioned above, there is a "strict" mode that enforces sanity checks. In particular, the date must be the entire contents of the string, and numbers are range-checked. If you have any suggestions on how to make this mode more strict, contact me. + +Unparsing +--------- + +I use Rick McCarty's algorithm for converting calendar dates to week dates (http://personal.ecu.edu/mccartyr/ISOwdAlg.txt), slightly tweaked. + +Bugs +==== + +Parsing +------- + +* This method won't extract a date from just anywhere in a string, only immediately after the start of the string (or any leading whitespace). There are two solutions: either require you to invoke the parser on a string that is only an ISO 8601 date, with nothing before or after (bad for parsing purposes), or make the parser able to find an ISO 8601 date as a substring. I won't do the first one, and barring a patch, I probably won't do the second one either. + +* Date ranges (also specified by ISO 8601) are not supported; this method will only return one date. To handle ranges would require at least one more method. + +* There is no method to analyze a date string and tell you what was found in it (year, month, week, day, ordinal day, etc.). Feel free to submit a patch. + +Copyright +========= + +This code is copyright 2006–2011 Peter Hosey. It is under the BSD license; see LICENSE.txt for the full text of the license. diff --git a/uispec.opts b/uispec.opts deleted file mode 100644 index 86b064ab73..0000000000 --- a/uispec.opts +++ /dev/null @@ -1,3 +0,0 @@ ---workspace RestKit.xcodeproj/project.xcworkspace ---scheme "RestKit Specs" ---sdk 4.3