Permalink
Browse files

Allow chaining response processors, and add a new AEExpect module to …

…let you create some common blocks for response validity checking.
  • Loading branch information...
1 parent 5b702b1 commit 4e0669e10410132a535b167b738760ecff34f9ac @adamjernst committed Oct 14, 2011
View
@@ -0,0 +1,39 @@
+//
+// AEExpect.h
+// AEURLExample
+//
+// Created by Adam Ernst on 10/13/11.
+// Copyright (c) 2011 cosmicsoft. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "AEURLConnection.h"
+
+extern NSString *AEExpectErrorDomain;
+
+typedef enum {
+ AEExpectInvalidStatusCodeError = -101,
+ AEExpectResponseNotHTTPError = -102,
+ AEExpectInvalidContentTypeError = -103,
+ AEExpectInvalidResponseClassError = -104,
+} AEExpectErrorCode;
+
+@interface AEExpect : NSObject
+
+// Sets an error if the HTTP status code is not in the provided set.
++ (AEURLResponseProcessor)statusCode:(NSIndexSet *)acceptableCodes;
+
+// All 200 status codes
++ (NSIndexSet *)defaultAcceptableStatusCodes;
+
+// Sets an error if the Content-Type header does not match one of the included
+// acceptable content types, after removing any "charset" or other parameters.
+// See [AEJSONProcessor defaultAcceptableJSONContentTypes] for an example set.
++ (AEURLResponseProcessor)contentType:(NSSet *)acceptableTypes;
+
+// Sets an error if the passed data is not an instance of a certain class.
+// Handy for use after an AEJSONProcessor, if you want to ensure that
+// you're getting a dictionary vs. an array.
++ (AEURLResponseProcessor)responseClass:(Class)class;
+
+@end
View
@@ -0,0 +1,76 @@
+//
+// AEExpect.m
+// AEURLExample
+//
+// Created by Adam Ernst on 10/13/11.
+// Copyright (c) 2011 cosmicsoft. All rights reserved.
+//
+
+#import "AEExpect.h"
+
+NSString *AEExpectErrorDomain = @"AEExpectErrorDomain";
+
+@implementation AEExpect
+
++ (NSError *)error:(AEExpectErrorCode)code message:(NSString *)message {
+ return [NSError errorWithDomain:AEExpectErrorDomain
+ code:code
+ userInfo:[NSDictionary dictionaryWithObject:message
+ forKey:NSLocalizedDescriptionKey]];
+}
+
++ (AEURLResponseProcessor)statusCode:(NSIndexSet *)acceptableCodes {
+ return [[^(NSURLResponse *response, id data, NSError **error){
+ if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
+ *error = [AEExpect error:AEExpectResponseNotHTTPError
+ message:@"Response is not HTTP"];
+ return nil;
+ }
+
+ NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
+ if (![acceptableCodes containsIndex:statusCode]) {
+ *error = [AEExpect error:AEExpectInvalidStatusCodeError
+ message:[NSString stringWithFormat:@"%@ (HTTP status %d)",
+ [NSHTTPURLResponse localizedStringForStatusCode:statusCode],
+ statusCode]];
+ return nil;
+ }
+
+ return data;
+ } copy] autorelease];
+}
+
++ (NSIndexSet *)defaultAcceptableStatusCodes {
+ return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
+}
+
+// Sets an error if the Content-Type header does not match one of the included
+// acceptable content types, after removing any "charset" or other parameters.
++ (AEURLResponseProcessor)contentType:(NSSet *)acceptableTypes {
+ return [[^(NSURLResponse *response, id data, NSError **error) {
+ if (![acceptableTypes containsObject:[response MIMEType]]) {
+ *error = [AEExpect error:AEExpectInvalidContentTypeError
+ message:[NSString stringWithFormat:@"Invalid Content-Type %@", [response MIMEType]]];
+ return nil;
+ }
+
+ return data;
+ } copy] autorelease];
+}
+
+// Sets an error if the passed data is not an instance of a certain class.
+// Handy for use after an AEJSONProcessor, if you want to ensure that
+// you're getting a dictionary vs. an array.
++ (AEURLResponseProcessor)responseClass:(Class)class {
+ return [[^(NSURLResponse *response, id data, NSError **error) {
+ if (![data isKindOfClass:class]) {
+ *error = [AEExpect error:AEExpectInvalidResponseClassError
+ message:[NSString stringWithFormat:@"Invalid response class %@", NSStringFromClass([data class])]];
+ return nil;
+ }
+
+ return data;
+ } copy] autorelease];
+}
+
+@end
@@ -0,0 +1,34 @@
+//
+// AEJSONProcessor.h
+// AEURLExample
+//
+// Created by Adam Ernst on 10/13/11.
+// Copyright (c) 2011 cosmicsoft. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "AEURLConnection.h"
+#import "AEURLRequestFactory.h"
+
+// AEJSONProcessor requires JSONKit. You can use AEURLConnection
+// without JSONKit; just remove the AEJSONProcessor.m/h files from your
+// project, and parse JSON manually.
+#import "JSONKit.h"
+
+
+@interface AEJSONProcessor : NSObject
+
+// These blocks are used to process a response from the server.
++ (AEURLResponseProcessor)JSONResponseProcessor;
++ (AEURLResponseProcessor)JSONResponseProcessorWithOptions:(JKParseOptionFlags)options;
+
+// This block will put parameters into a NSMutableURLRequest's HTTP body,
+// encoded as JSON, and set the request's Content-Type header to
+// "application/json; charset=UTF-8".
++ (AEURLParameterProcessor)JSONParameterProcessor;
+
+// A set with the most common Content-Types for JSON. Handy with the
+// [AEExpect contentType:] response processor, when used in a chain.
++ (NSSet *)defaultAcceptableJSONContentTypes;
+
+@end
@@ -0,0 +1,40 @@
+//
+// AEJSONProcessor.m
+// AEURLExample
+//
+// Created by Adam Ernst on 10/13/11.
+// Copyright (c) 2011 cosmicsoft. All rights reserved.
+//
+
+#import "AEJSONProcessor.h"
+
+@implementation AEJSONProcessor
+
+static AEURLResponseProcessor JSONProcessor = nil;
+
++ (AEURLResponseProcessor)JSONResponseProcessor {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ JSONProcessor = [[self JSONResponseProcessorWithOptions:JKParseOptionNone] retain];
+ });
+ return JSONProcessor;
+}
+
++ (AEURLResponseProcessor)JSONResponseProcessorWithOptions:(JKParseOptionFlags)options {
+ return [[(id)^(NSURLResponse *response, NSData *data, NSError **error){
+ return [data objectFromJSONDataWithParseOptions:options error:error];
+ } copy] autorelease];
+}
+
++ (AEURLParameterProcessor)JSONParameterProcessor {
+ return [[^(NSDictionary *parameters, NSMutableURLRequest *targetRequest){
+ [targetRequest setHTTPBody:[parameters JSONData]];
+ [targetRequest setValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
+ } copy] autorelease];
+}
+
++ (NSSet *)defaultAcceptableJSONContentTypes {
+ return [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
+}
+
+@end
@@ -8,7 +8,11 @@
#import <Foundation/Foundation.h>
-typedef id (^AEURLConnectionResponseProcessingBlock)(NSURLResponse *, NSData *, NSError **);
+// This block can be used as a parameter to |processor:|.
+// It is REQUIRED that the block either returns an object and leaves the error
+// parameter untouched, or sets an error and returns nil. This is enforced with
+// an assertion.
+typedef id (^AEURLResponseProcessor)(NSURLResponse *, NSData *, NSError **);
@interface AEURLConnection : NSObject
@@ -20,14 +24,32 @@ typedef id (^AEURLConnectionResponseProcessingBlock)(NSURLResponse *, NSData *,
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler;
// You may want to process the response on a background thread, but still safely
-// receive the response on |queue|. This method runs |processingBlock| on a
+// receive the response on |queue|. This method runs |processor| on a
// low-priority serial queue (one operation at a time, to prevent thrashing the
// CPU). completionHandler returns the result of the processing block, instead
// of an NSData* object.
-// Check out AEJSONProcessingBlock for an example usage.
+// Check out AEJSONProcessor for an example usage.
+ (void)sendAsynchronousRequest:(NSURLRequest *)request
queue:(NSOperationQueue *)queue
- processingBlock:(AEURLConnectionResponseProcessingBlock)processingBlock
+ processor:(AEURLResponseProcessor)processor
completionHandler:(void (^)(NSURLResponse *, id, NSError *))handler;
+// The real power comes when you chain processors together. This allows
+// you to verify that the status code is acceptable, then that the content-type
+// is what you expect, then parse JSON, and finally require that the response
+// is a dictionary (not an array)--and to do it all in a declarative way.
+// This means you can store the chained processor in your app and reuse it in
+// different contexts, instead of duplicating the logic everywhere.
+
+// Note that because completionHandler returns *either* an NSError *or* data,
+// never both, you cannot get the document data if a response processor fails.
+// If you need to access the HTTP body, you'll need to do your response
+// processing the old fashioned way.
+
+// The returned processor will run each processor in sequence. If one fails by
+// returning an NSError*, processing stops immediately and the subsequent
+// processors are not run.
+// Just like NSArray, *the last argument must be nil.*
++ (AEURLResponseProcessor)chainedResponseProcessor:(AEURLResponseProcessor)firstProcessor, ... NS_REQUIRES_NIL_TERMINATION;
+
@end
@@ -12,14 +12,14 @@
@interface AEURLConnectionRequest : NSObject
- (id)initWithRequest:(NSURLRequest *)request
queue:(NSOperationQueue *)queue
- processingBlock:(AEURLConnectionResponseProcessingBlock)processingBlock
+ processor:(AEURLResponseProcessor)processor
completionHandler:handler;
@property (nonatomic, retain, readonly) NSURLRequest *request;
@property (nonatomic, retain, readonly) NSOperationQueue *queue;
-// processingBlock released in the background, so don't capture a
+// processor released in the background, so don't capture a
// UIViewController or you'll be vulnerable to the Deallocation Problem.
-@property (nonatomic, copy, readonly) AEURLConnectionResponseProcessingBlock processingBlock;
+@property (nonatomic, copy, readonly) AEURLResponseProcessor processor;
// handler is readwrite so that we can nil it out after calling it,
// to ensure it is released on |queue| and not on the network thread.
@@ -45,20 +45,39 @@ @implementation AEURLConnection
+ (void)sendAsynchronousRequest:(NSURLRequest *)request
queue:(NSOperationQueue*)queue
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler {
- AEURLConnectionRequest *req = [[AEURLConnectionRequest alloc] initWithRequest:request queue:queue processingBlock:nil completionHandler:handler];
- [[AEURLConnectionManager sharedManager] startRequest:req];
- [req release];
+ return [AEURLConnection sendAsynchronousRequest:request queue:queue processor:nil completionHandler:handler];
}
+ (void)sendAsynchronousRequest:(NSURLRequest *)request
queue:(NSOperationQueue *)queue
- processingBlock:(AEURLConnectionResponseProcessingBlock)processingBlock
+ processor:(AEURLResponseProcessor)processor
completionHandler:(void (^)(NSURLResponse *, id, NSError *))handler {
- AEURLConnectionRequest *req = [[AEURLConnectionRequest alloc] initWithRequest:request queue:queue processingBlock:processingBlock completionHandler:handler];
+ AEURLConnectionRequest *req = [[AEURLConnectionRequest alloc] initWithRequest:request queue:queue processor:processor completionHandler:handler];
[[AEURLConnectionManager sharedManager] startRequest:req];
[req release];
}
++ (AEURLResponseProcessor)chainedResponseProcessor:(AEURLResponseProcessor)firstProcessor, ... {
+ NSMutableArray *processors = [NSMutableArray array];
+ va_list args;
+ va_start(args, firstProcessor);
+ for (AEURLResponseProcessor processor = firstProcessor; processor != nil; processor = va_arg(args, AEURLResponseProcessor)) {
+ [processors addObject:[[processor copy] autorelease]];
+ }
+
+ return [[^(NSURLResponse *response, id data, NSError **error) {
+ id newData = data;
+ for (AEURLResponseProcessor processor in processors) {
+ newData = processor(response, newData, error);
+ if (*error) {
+ NSAssert(newData == nil, @"Expected data or error but not both");
+ return nil;
+ }
+ }
+ return newData;
+ } copy] autorelease];
+}
+
@end
@@ -232,26 +251,28 @@ - (void)safelyCallCompletionHandler:(AEURLConnectionRequest *)req error:(NSError
- (void)executeHandlerForConnection:(NSURLConnection *)connection error:(NSError *)error {
AEURLConnectionRequest *req = [self executingRequestForConnection:connection];
- if ([req processingBlock]) {
+ if (!error && [req processor]) {
// Create a serial queue to avoid thrashing the CPU.
static dispatch_queue_t processing_queue;
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
processing_queue = dispatch_queue_create("com.adamernst.AEURLConnection.processing", 0);
+ // Don't use DISPATCH_QUEUE_PRIORITY_BACKGROUND as it doesn't exist
+ // on earlier versions of iOS.
+ dispatch_set_target_queue(processing_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
});
dispatch_async(processing_queue, ^{
- AEURLConnectionResponseProcessingBlock processor = [req processingBlock];
- NSError *error = nil;
- id processedData = processor([req response], [req data], &error);
- if (processedData) {
- [self safelyCallCompletionHandler:req error:nil data:processedData];
- } else {
- [self safelyCallCompletionHandler:req error:error data:processedData];
- }
+ AEURLResponseProcessor processor = [req processor];
+ NSError *processorError = nil;
+ id processedData = processor([req response], [req data], &processorError);
+ // Note that the block is required to either return data and leave
+ // error untouched, or set an error and return nil. This is enforced
+ // with an assertion in |safelyCallCompletionHandler:error:data:|.
+ [self safelyCallCompletionHandler:req error:processorError data:processedData];
});
} else {
- [self safelyCallCompletionHandler:req error:error data:[req data]];
+ [self safelyCallCompletionHandler:req error:error data:error ? nil : [req data]];
}
// Don't remove |req| from |_executingRequests| until this point. Since
@@ -280,7 +301,7 @@ @implementation AEURLConnectionRequest
@synthesize request=_request;
@synthesize queue=_queue;
-@synthesize processingBlock=_processingBlock;
+@synthesize processor=_processor;
@synthesize handler=_handler;
@synthesize connection=_connection;
@@ -289,13 +310,13 @@ @implementation AEURLConnectionRequest
- (id)initWithRequest:(NSURLRequest *)request
queue:(NSOperationQueue *)queue
- processingBlock:(AEURLConnectionResponseProcessingBlock)processingBlock
+ processor:(AEURLResponseProcessor)processor
completionHandler:(id)handler {
self = [super init];
if (self) {
_request = [request retain];
_queue = [queue retain];
- _processingBlock = [processingBlock copy];
+ _processor = [processor copy];
_handler = [handler copy];
}
return self;
@@ -304,7 +325,7 @@ - (id)initWithRequest:(NSURLRequest *)request
- (void)dealloc {
[_request release];
[_queue release];
- [_processingBlock release];
+ [_processor release];
[_handler release];
[_connection release];
Oops, something went wrong.

0 comments on commit 4e0669e

Please sign in to comment.