Skip to content

Commit

Permalink
Allow chaining response processors, and add a new AEExpect module to …
Browse files Browse the repository at this point in the history
…let you create some common blocks for response validity checking.
  • Loading branch information
adamjernst committed Oct 14, 2011
1 parent 5b702b1 commit b989dca
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 93 deletions.
39 changes: 39 additions & 0 deletions AEURLConnection/AEExpect.h
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions AEURLConnection/AEExpect.m
Original file line number Diff line number Diff line change
@@ -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
30 changes: 0 additions & 30 deletions AEURLConnection/AEJSONProcessingBlock.h

This file was deleted.

34 changes: 34 additions & 0 deletions AEURLConnection/AEJSONProcessor.h
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
30 changes: 26 additions & 4 deletions AEURLConnection/AEURLConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Loading

0 comments on commit b989dca

Please sign in to comment.