Add APIs to RKObjectManager for use in firing batches of operations.

This commit is contained in:
Jeff Arena
2012-09-30 13:43:06 -04:00
parent 9743da4f0f
commit c301060ab8
5 changed files with 202 additions and 25 deletions

View File

@@ -406,6 +406,37 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
*/
- (void)cancelAllObjectRequestOperationsWithMethod:(RKRequestMethod)method matchingPathPattern:(NSString *)pathPattern;
///--------------------------------------------------
/// @name Managing Batches of Enqueued Object Request Operations
///--------------------------------------------------
/**
Enqueues a set of `RKObjectRequestOperation`, derived from the provided RKRoute and objects, to the object manager's operation queue.
@param route The RKRoute to apply to all provided objects.
@param objects The set of objects that should be turned into operations using the provided route.
@param progress A block object to be executed when an object request operation completes. This block has no return value and takes two arguments: the number of finished operations and the total number of operations initially executed.
@param completion A block object to be executed when the object request operations complete. This block has no return value and takes one argument: the list of operations executed.
@see [RKObjectManager enqueueBatchOfObjectRequestOperations:progress:completion]
*/
- (void)enqueueBatchOfObjectRequestOperationsWithRoute:(RKRoute *)route
objects:(NSArray *)objects
progress:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progress
completion:(void (^)(NSArray *operations))completion;
/**
Enqueues a set of `RKObjectRequestOperation` to the object manager's operation queue.
@param operations The set of object request operations to be enqueued.
@param progress A block object to be executed when an object request operation completes. This block has no return value and takes two arguments: the number of finished operations and the total number of operations initially executed.
@param completion A block object to be executed when the object request operations complete. This block has no return value and takes one argument: the list of operations executed.
*/
- (void)enqueueBatchOfObjectRequestOperations:(NSArray *)operations
progress:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progress
completion:(void (^)(NSArray *operations))completion;
///-------------------------------------
/// @name Making Object Requests by Path
///-------------------------------------

View File

@@ -109,24 +109,6 @@ static BOOL RKDoesArrayOfResponseDescriptorsContainEntityMapping(NSArray *respon
return NO;
}
static char kRKBaseURLAssociatedObjectKey;
void RKAssociateBaseURLWithURL(NSURL *baseURL, NSURL *URL)
{
NSCAssert(baseURL, @"baseURL cannot be nil");
NSCAssert(URL, @"URL cannot be nil");
objc_setAssociatedObject(URL,
&kRKBaseURLAssociatedObjectKey,
baseURL,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
NSURL *RKBaseURLAssociatedWithURL(NSURL *URL)
{
NSCAssert(URL, @"URL cannot be nil");
return objc_getAssociatedObject(URL, &kRKBaseURLAssociatedObjectKey);
}
///////////////////////////////////
@interface RKObjectManager ()
@@ -533,6 +515,70 @@ NSURL *RKBaseURLAssociatedWithURL(NSURL *URL)
}
}
- (void)enqueueBatchOfObjectRequestOperationsWithRoute:(RKRoute *)route
objects:(NSArray *)objects
progress:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progress
completion:(void (^)(NSArray *operations))completion {
NSMutableArray *operations = [[NSMutableArray alloc] initWithCapacity:objects.count];
for (id object in objects) {
RKObjectRequestOperation *operation = nil;
NSURL *URL = [self.router URLForRoute:route object:object];
NSAssert(URL, @"Failed to generate URL for route %@ with object %@", route, object);
if ([route isClassRoute]) {
operation = [self appropriateObjectRequestOperationWithObject:object method:RKRequestMethodGET path:[URL relativeString] parameters:nil];
} else {
operation = [self appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:[URL relativeString] parameters:nil];
}
[operations addObject:operation];
}
return [self enqueueBatchOfObjectRequestOperations:operations progress:progress completion:completion];
}
- (void)enqueueBatchOfObjectRequestOperations:(NSArray *)operations
progress:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progress
completion:(void (^)(NSArray *operations))completion {
__block dispatch_group_t dispatchGroup = dispatch_group_create();
NSBlockOperation *batchedOperation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
if (completion) {
completion(operations);
}
});
}];
for (RKObjectRequestOperation *operation in operations) {
void (^originalCompletionBlock)(void) = [operation.completionBlock copy];
[operation setCompletionBlock:^{
dispatch_queue_t queue = operation.successCallbackQueue ?: dispatch_get_main_queue();
dispatch_group_async(dispatchGroup, queue, ^{
if (originalCompletionBlock) {
originalCompletionBlock();
}
__block NSUInteger numberOfFinishedOperations = 0;
[operations enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([(NSOperation *)obj isFinished]) {
numberOfFinishedOperations++;
}
}];
if (progress) {
progress(numberOfFinishedOperations, [operations count]);
}
dispatch_group_leave(dispatchGroup);
});
}];
dispatch_group_enter(dispatchGroup);
[batchedOperation addDependency:operation];
[self enqueueObjectRequestOperation:operation];
}
[self.operationQueue addOperation:batchedOperation];
}
@end
NSString *RKStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus networkReachabilityStatus)

View File

@@ -21,6 +21,10 @@
#import "RKHTTPUtilities.h"
@class RKRouteSet;
@class RKRoute;
void RKAssociateBaseURLWithURL(NSURL *baseURL, NSURL *URL);
NSURL *RKBaseURLAssociatedWithURL(NSURL *URL);
/**
An `RKRouter` instance is responsible for generating `NSURL` objects with a given base URL and a route set. It is used to centralize the knowledge about the URL's that are used by the application.
@@ -106,6 +110,8 @@
*/
- (NSURL *)URLForRelationship:(NSString *)relationshipName ofObject:(id)object method:(RKRequestMethod)method;
- (NSURL *)URLForRoute:(RKRoute *)route object:(id)object;
///---------------------------------------------
/// @name Configuring the Base URL and Route Set
///---------------------------------------------

View File

@@ -22,6 +22,7 @@
#import "RKRouteSet.h"
#import "RKRoute.h"
#import "RKPathMatcher.h"
#import <objc/runtime.h>
@interface RKRouter ()
@@ -29,6 +30,23 @@
@property (nonatomic, strong, readwrite) RKRouteSet *routeSet;
@end
static char kRKBaseURLAssociatedObjectKey;
void RKAssociateBaseURLWithURL(NSURL *baseURL, NSURL *URL)
{
NSCAssert(baseURL, @"baseURL cannot be nil");
NSCAssert(URL, @"URL cannot be nil");
objc_setAssociatedObject(URL,
&kRKBaseURLAssociatedObjectKey,
baseURL,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
NSURL *RKBaseURLAssociatedWithURL(NSURL *URL)
{
NSCAssert(URL, @"URL cannot be nil");
return objc_getAssociatedObject(URL, &kRKBaseURLAssociatedObjectKey);
}
@implementation RKRouter
@@ -54,23 +72,32 @@
- (NSURL *)URLForRouteNamed:(NSString *)routeName method:(out RKRequestMethod *)method object:(id)object
{
RKRoute *route = [self.routeSet routeForName:routeName];
if (! route) return nil;
if (method) *method = route.method;
return [NSURL URLWithString:[self pathFromRoute:route forObject:object] relativeToURL:self.baseURL];
return [self URLForRoute:route object:object];
}
- (NSURL *)URLForObject:(id)object method:(RKRequestMethod)method
{
RKRoute *route = [self.routeSet routeForObject:object method:method];
if (! route) return nil;
return [NSURL URLWithString:[self pathFromRoute:route forObject:object] relativeToURL:self.baseURL];
return [self URLForRoute:route object:object];
}
- (NSURL *)URLForRelationship:(NSString *)relationshipName ofObject:(id)object method:(RKRequestMethod)method
{
RKRoute *route = [self.routeSet routeForRelationship:relationshipName ofClass:[object class] method:method];
if (! route) return nil;
return [NSURL URLWithString:[self pathFromRoute:route forObject:object] relativeToURL:self.baseURL];
return [self URLForRoute:route object:object];
}
- (NSURL *)URLForRoute:(RKRoute *)route object:(id)object
{
NSParameterAssert(route);
NSURL *URL = [NSURL URLWithString:[self pathFromRoute:route forObject:object] relativeToURL:self.baseURL];
/**
Associate our baseURL with the URL of the `NSURLRequest` object. This enables us to match response descriptors by path.
*/
RKAssociateBaseURLWithURL(self.baseURL, URL);
return URL;
}
- (NSString *)pathFromRoute:(RKRoute *)route forObject:(id)object

View File

@@ -32,6 +32,10 @@ NSURL *RKBaseURLAssociatedWithURL(NSURL *URL);
@interface RKObjectManagerTest : RKTestCase
@property (nonatomic, strong) RKObjectManager *objectManager;
@property (nonatomic, strong) RKRoute *humanGETRoute;
@property (nonatomic, strong) RKRoute *humanPOSTRoute;
@property (nonatomic, strong) RKRoute *humanCatsRoute;
@property (nonatomic, strong) RKRoute *humansCollectionRoute;
@end
@@ -77,7 +81,16 @@ NSURL *RKBaseURLAssociatedWithURL(NSURL *URL);
RKObjectMapping *humanSerialization = [RKObjectMapping mappingForClass:[NSDictionary class]];
[humanSerialization addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]];
[self.objectManager addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping:humanSerialization objectClass:[RKHuman class] rootKeyPath:@"human"]];
[self.objectManager.router.routeSet addRoute:[RKRoute routeWithClass:[RKHuman class] pathPattern:@"/humans" method:RKRequestMethodPOST]];
self.humanPOSTRoute = [RKRoute routeWithClass:[RKHuman class] pathPattern:@"/humans" method:RKRequestMethodPOST];
self.humanGETRoute = [RKRoute routeWithClass:[RKHuman class] pathPattern:@"/humans/:railsID" method:RKRequestMethodGET];
self.humanCatsRoute = [RKRoute routeWithRelationshipName:@"cats" objectClass:[RKHuman class] pathPattern:@"/humans/:railsID/cats" method:RKRequestMethodGET];
self.humansCollectionRoute = [RKRoute routeWithName:@"humans" pathPattern:@"/humans" method:RKRequestMethodGET];
[self.objectManager.router.routeSet addRoute:self.humanPOSTRoute];
[self.objectManager.router.routeSet addRoute:self.humanGETRoute];
[self.objectManager.router.routeSet addRoute:self.humanCatsRoute];
[self.objectManager.router.routeSet addRoute:self.humansCollectionRoute];
}
- (void)tearDown
@@ -168,6 +181,60 @@ NSURL *RKBaseURLAssociatedWithURL(NSURL *URL);
assertThatBool([operation isCancelled], is(equalToBool(NO)));
}
- (void)testShouldProperlyFireABatchOfOperations
{
RKHuman *temporaryHuman = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:_objectManager.managedObjectStore.primaryManagedObjectContext];
temporaryHuman.name = @"My Name";
RKManagedObjectRequestOperation *successfulGETOperation = [_objectManager appropriateObjectRequestOperationWithObject:temporaryHuman method:RKRequestMethodGET path:nil parameters:nil];
RKManagedObjectRequestOperation *successfulPOSTOperation = [_objectManager appropriateObjectRequestOperationWithObject:temporaryHuman method:RKRequestMethodPOST path:nil parameters:nil];
RKManagedObjectRequestOperation *failedPOSTOperation = [_objectManager appropriateObjectRequestOperationWithObject:temporaryHuman method:RKRequestMethodPOST path:@"/humans/fail" parameters:nil];
__block NSUInteger progressCallbackCount = 0;
__block NSUInteger completionBlockOperationCount = 0;
[_objectManager enqueueBatchOfObjectRequestOperations:@[successfulGETOperation, successfulPOSTOperation, failedPOSTOperation] progress:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
progressCallbackCount++;
} completion:^(NSArray *operations) {
completionBlockOperationCount = operations.count;
}];
assertThat(_objectManager.operationQueue, is(notNilValue()));
[_objectManager.operationQueue waitUntilAllOperationsAreFinished];
// Spin the run loop to allow completion blocks to fire after operations have completed
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
assertThatInteger(progressCallbackCount, is(equalToInteger(3)));
assertThatInteger(completionBlockOperationCount, is(equalToInteger(3)));
}
- (void)testShouldProperlyFireABatchOfOperationsFromRoute
{
RKHuman *dan = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:_objectManager.managedObjectStore.primaryManagedObjectContext];
dan.name = @"Dan";
RKHuman *blake = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:_objectManager.managedObjectStore.primaryManagedObjectContext];
blake.name = @"Blake";
RKHuman *jeff = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:_objectManager.managedObjectStore.primaryManagedObjectContext];
jeff.name = @"Jeff";
__block NSUInteger progressCallbackCount = 0;
__block NSUInteger completionBlockOperationCount = 0;
[_objectManager enqueueBatchOfObjectRequestOperationsWithRoute:self.humanPOSTRoute objects:@[dan, blake, jeff] progress:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
progressCallbackCount++;
} completion:^(NSArray *operations) {
completionBlockOperationCount = operations.count;
}];
assertThat(_objectManager.operationQueue, is(notNilValue()));
[_objectManager.operationQueue waitUntilAllOperationsAreFinished];
// Spin the run loop to allow completion blocks to fire after operations have completed
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
assertThatInteger(progressCallbackCount, is(equalToInteger(3)));
assertThatInteger(completionBlockOperationCount, is(equalToInteger(3)));
}
// TODO: Move to Core Data specific spec file...
//- (void)testShouldLoadAHuman
//{