Add support for returning fetched objects when a 304 'Not Modified' response is loaded. closes #1006

This commit is contained in:
Blake Watters
2012-10-28 21:44:42 -04:00
parent 5cab23b3af
commit 94318d1cb2
4 changed files with 98 additions and 19 deletions

View File

@@ -88,6 +88,10 @@
## Managed Object Context Save Behaviors
The results of the operation can either be 'pushed' to the parent context or saved to the persistent store. Configuration is available via the `savesToPersistentStore` property. If an error is encountered while saving the managed object context, then the operation is considered to have failed and the `error` property will be set to the `NSError` object returned by the failed save.
## 304 'Not Modified' Responses
In the event that a managed object request operation loads a 304 'Not Modified' response for an HTTP request no object mapping is performed as Core Data is assumed to contain a managed object representation of the resource requested. No object mapping is performed on the cached response body, making a cache hit for a managed object request operation a very lightweight operation. To build the mapping result returned to the caller, all of the fetch request blocks matching the request URL will be invoked and each fetch request returned is executed against the managed object context and the objects returned are added to the mapping result. Please note that all managed objects returned in the mapping result for a 'Not Modified' response will be returned under the `[NSNull null]` key path.
## Limitations and Caveats
@@ -165,10 +169,10 @@
typedef NSFetchRequest *(^RKFetchRequestBlock)(NSURL *URL);
/**
Returns a fetch request object from an array of `RKFetchRequestBlock` objects given a URL.
Returns an array of fetch request objects from an array of `RKFetchRequestBlock` objects given a URL.
@param fetchRequestBlocks An array of `RKFetchRequestBlock` blocks to
@param URL The URL for which to return a fetch request.
@return A fetch request from the first block that matches the URL.
@return An array of fetch requests from all blocks that match the given URL.
*/
NSFetchRequest *RKFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL);
NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL);

View File

@@ -29,14 +29,15 @@
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitCoreData
NSFetchRequest *RKFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL)
NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL)
{
NSMutableArray *fetchRequests = [NSMutableArray array];
NSFetchRequest *fetchRequest = nil;
for (RKFetchRequestBlock block in [fetchRequestBlocks reverseObjectEnumerator]) {
fetchRequest = block(URL);
if (fetchRequest) break;
if (fetchRequest) [fetchRequests addObject:fetchRequest];
}
return fetchRequest;
return fetchRequests;
}
static NSDictionary *RKDictionaryOfManagedObjectsInContextFromDictionaryOfManagedObjects(NSDictionary *dictionaryOfManagedObjects, NSManagedObjectContext *managedObjectContext)
@@ -69,6 +70,20 @@ static NSDictionary *RKDictionaryOfManagedObjectsInContextFromDictionaryOfManage
return newDictionary;
}
static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *responseDescriptors)
{
NSCParameterAssert(URL);
NSCParameterAssert(responseDescriptors);
NSArray *baseURLs = [responseDescriptors valueForKeyPath:@"@distinctUnionOfObjects.baseURL"];
if ([baseURLs count] == 1) {
NSURL *baseURL = baseURLs[0];
NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(URL, baseURL);
URL = [NSURL URLWithString:pathAndQueryString relativeToURL:baseURL];
}
return URL;
}
@interface RKManagedObjectRequestOperation () <RKMapperOperationDelegate>
// Core Data specific
@property (nonatomic, strong) NSManagedObjectContext *privateContext;
@@ -134,8 +149,21 @@ static NSDictionary *RKDictionaryOfManagedObjectsInContextFromDictionaryOfManage
{
if (self.HTTPRequestOperation.wasNotModified) {
RKLogDebug(@"Managed object mapping requested for cached response: skipping mapping...");
// TODO: This is unexpectedly returning an empty result set... need to be able to retrieve the appropriate objects...
return [[RKMappingResult alloc] initWithDictionary:@{}];
NSURL *URL = RKRelativeURLFromURLAndResponseDescriptors(self.HTTPRequestOperation.response.URL, self.responseDescriptors);
NSArray *fetchRequests = RKArrayOfFetchRequestFromBlocksWithURL(self.fetchRequestBlocks, URL);
NSMutableArray *managedObjects = [NSMutableArray array];
[self.managedObjectContext performBlockAndWait:^{
NSError *error = nil;
for (NSFetchRequest *fetchRequest in fetchRequests) {
NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects) {
[managedObjects addObjectsFromArray:fetchedObjects];
} else {
RKLogError(@"Failed to execute fetch request %@: %@", fetchRequest, error);
}
}
}];
return [[RKMappingResult alloc] initWithDictionary:@{ [NSNull null]: managedObjects }];
}
self.responseMapperOperation = [[RKManagedObjectResponseMapperOperation alloc] initWithResponse:self.HTTPRequestOperation.response
@@ -190,14 +218,7 @@ static NSDictionary *RKDictionaryOfManagedObjectsInContextFromDictionaryOfManage
__block NSArray *_blockObjects;
// Pass the fetch request blocks a relative `NSURL` object if possible
NSURL *URL = [self.HTTPRequestOperation.request URL];
NSArray *baseURLs = [self.responseDescriptors valueForKeyPath:@"@distinctUnionOfObjects.baseURL"];
if ([baseURLs count] == 1) {
NSURL *baseURL = baseURLs[0];
NSString *pathAndQueryString = RKPathAndQueryStringFromURLRelativeToURL(URL, baseURL);
URL = [NSURL URLWithString:pathAndQueryString relativeToURL:baseURL];
}
NSURL *URL = RKRelativeURLFromURLAndResponseDescriptors(self.HTTPRequestOperation.response.URL, self.responseDescriptors);
for (RKFetchRequestBlock fetchRequestBlock in [self.fetchRequestBlocks reverseObjectEnumerator]) {
NSFetchRequest *fetchRequest = fetchRequestBlock(URL);
if (fetchRequest) {

View File

@@ -9,6 +9,7 @@
#import "RKTestEnvironment.h"
#import "RKManagedObjectRequestOperation.h"
#import "RKEntityMapping.h"
#import "RKHuman.h"
@interface RKManagedObjectRequestOperation ()
- (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error;
@@ -21,6 +22,16 @@
@implementation RKManagedObjectRequestOperationTest
- (void)setUp
{
[RKTestFactory setUp];
}
- (void)tearDown
{
[RKTestFactory tearDown];
}
- (void)testThatInitializationWithRequestDefaultsToSavingToPersistentStore
{
RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:@"http://restkit.org"]];
@@ -42,10 +53,15 @@
{
NSURL *baseURL = [NSURL URLWithString:@"http://restkit.org/api/v1/"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"categories/1234" relativeToURL:baseURL]];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"1.1" headerFields:@{}];
RKObjectMapping *mapping = [RKObjectMapping requestMapping];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:@"categories/:categoryID" keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]];
responseDescriptor.baseURL = baseURL;
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[responseDescriptor]];
id mockRequestOperation = [OCMockObject niceMockForClass:[RKHTTPRequestOperation class]];
[[[mockRequestOperation stub] andReturn:request] request];
[[[mockRequestOperation stub] andReturn:response] response];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:mockRequestOperation responseDescriptors:@[ responseDescriptor ]];
__block NSURL *blockURL = nil;
RKFetchRequestBlock fetchRequesBlock = ^NSFetchRequest *(NSURL *URL) {
blockURL = URL;
@@ -64,11 +80,15 @@
{
NSURL *baseURL = [NSURL URLWithString:@"http://restkit.org/api/v1/"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"library/" relativeToURL:baseURL]];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"1.1" headerFields:@{}];
expect([request.URL absoluteString]).to.equal(@"http://restkit.org/api/v1/library/");
RKObjectMapping *mapping = [RKObjectMapping requestMapping];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping pathPattern:@"library/" keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]];
responseDescriptor.baseURL = baseURL;
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[responseDescriptor]];
id mockRequestOperation = [OCMockObject niceMockForClass:[RKHTTPRequestOperation class]];
[[[mockRequestOperation stub] andReturn:request] request];
[[[mockRequestOperation stub] andReturn:response] response];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:mockRequestOperation responseDescriptors:@[ responseDescriptor ]];
__block NSURL *blockURL = nil;
RKFetchRequestBlock fetchRequesBlock = ^NSFetchRequest *(NSURL *URL) {
blockURL = URL;
@@ -87,7 +107,6 @@
- (void)testThatMappingResultContainsObjectsFetchedFromManagedObjectContextTheOperationWasInitializedWith
{
// void RKSetIntermediateDictionaryValuesOnObjectForKeyPath(id object, NSString *keyPath);
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/all.json" relativeToURL:[RKTestFactory baseURL]]];
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"RKHuman" inManagedObjectStore:managedObjectStore];
@@ -105,4 +124,30 @@
expect(managedObjectContexts[0]).to.equal(managedObjectStore.mainQueueManagedObjectContext);
}
// 304 'Not Modified'
- (void)testThatManagedObjectsAreFetchedWhenHandlingANotModifiedResponse
{
RKFetchRequestBlock fetchRequestBlock = ^(NSURL *URL){
return [NSFetchRequest fetchRequestWithEntityName:@"RKHuman"];
};
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/204_with_not_modified_status" relativeToURL:[RKTestFactory baseURL]]];
RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"RKHuman" inManagedObjectStore:managedObjectStore];
[humanMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:@"name" toKeyPath:@"name"]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:humanMapping pathPattern:nil keyPath:@"human" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[responseDescriptor]];
managedObjectRequestOperation.fetchRequestBlocks = @[fetchRequestBlock];
managedObjectRequestOperation.managedObjectContext = managedObjectStore.mainQueueManagedObjectContext;
[managedObjectRequestOperation start];
[managedObjectRequestOperation waitUntilFinished];
expect(managedObjectRequestOperation.mappingResult).notTo.beNil();
NSArray *mappedObjects = [managedObjectRequestOperation.mappingResult array];
expect(mappedObjects).to.haveCountOf(1);
expect(mappedObjects[0]).to.equal(human);
}
@end

9
Tests/Server/server.rb Normal file → Executable file
View File

@@ -268,6 +268,15 @@ class RestKitTestServer < Sinatra::Base
sleep 0.05
status 204
end
get '/304' do
status 304
end
get '/204_with_not_modified_status' do
status 204
response.headers['Status'] = '304 Not Modified'
end
# start the server if ruby file executed directly
run! if app_file == $0