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 b989dca16e44fd780fd91d9dcc2db885825ae096 @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
@@ -1,30 +0,0 @@
-//
-// AEJSONProcessingBlock.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"
-
-// AEJSONProcessingBlock requires JSONKit. You can use AEURLConnection
-// without JSONKit; just remove the AEJSONProcessingBlock.m/h files from your
-// project, and parse JSON manually.
-#import "JSONKit.h"
-
-
-@interface AEJSONProcessingBlock : NSObject
-
-// These blocks are used to process a response from the server.
-+ (AEURLConnectionResponseProcessingBlock)JSONResponseProcessingBlock;
-+ (AEURLConnectionResponseProcessingBlock)JSONResponseProcessingBlockWithOptions:(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".
-+ (AEURLConnectionParameterProcessingBlock)JSONParameterProcessingBlock;
-
-@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
@@ -1,36 +1,40 @@
//
-// AEJSONProcessingBlock.m
+// AEJSONProcessor.m
// AEURLExample
//
// Created by Adam Ernst on 10/13/11.
// Copyright (c) 2011 cosmicsoft. All rights reserved.
//
-#import "AEJSONProcessingBlock.h"
+#import "AEJSONProcessor.h"
-@implementation AEJSONProcessingBlock
+@implementation AEJSONProcessor
-static AEURLConnectionResponseProcessingBlock JSONProcessingBlock = nil;
+static AEURLResponseProcessor JSONProcessor = nil;
-+ (AEURLConnectionResponseProcessingBlock)JSONResponseProcessingBlock {
++ (AEURLResponseProcessor)JSONResponseProcessor {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- JSONProcessingBlock = [[self JSONResponseProcessingBlockWithOptions:JKParseOptionNone] retain];
+ JSONProcessor = [[self JSONResponseProcessorWithOptions:JKParseOptionNone] retain];
});
- return JSONProcessingBlock;
+ return JSONProcessor;
}
-+ (AEURLConnectionResponseProcessingBlock)JSONResponseProcessingBlockWithOptions:(JKParseOptionFlags)options {
++ (AEURLResponseProcessor)JSONResponseProcessorWithOptions:(JKParseOptionFlags)options {
return [[(id)^(NSURLResponse *response, NSData *data, NSError **error){
return [data objectFromJSONDataWithParseOptions:options error:error];
} copy] autorelease];
}
-+ (AEURLConnectionParameterProcessingBlock)JSONParameterProcessingBlock {
++ (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
Oops, something went wrong.

0 comments on commit b989dca

Please sign in to comment.