Skip to content

Instantly share code, notes, and snippets.

@andreyvit
Created December 25, 2017 10:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save andreyvit/96cebf51ffeb2f40de5cb69c0f5c897f to your computer and use it in GitHub Desktop.
Save andreyvit/96cebf51ffeb2f40de5cb69c0f5c897f to your computer and use it in GitHub Desktop.
My template for server operations with using AFNetworking or any other 3rd-party code (in Objective-C)
@import Foundation;
typedef void (^MYPROJPromiseResultBlock)(id result);
typedef void (^MYPROJPromiseErrorBlock)(NSError *error);
typedef void (^MYPROJPromiseResultAndErrorBlock)(id result, NSError *error);
typedef void (^MYPROJPromiseErrorAndResultBlock)(NSError *error, id result);
typedef void (^MYPROJPromiseResultOrErrorBlock)(id resultOrError);
typedef void (^MYPROJPromiseWorkBlock)(MYPROJPromiseResultAndErrorBlock completionHandler);
extern NSString *MYPROJPromiseErrorDomain;
typedef NS_ENUM(NSInteger, MYPROJPromiseErrorCode) {
MYPROJPromiseErrorCodeCanceled = 1,
};
extern NSString *MYPROJPromiseUserInfoDebugDescriptionKey;
extern NSString *MYPROJPromiseUserInfoURLSessionDataTaskKey;
@interface MYPROJPromise<ResultType> : NSObject
+ (void)setUnhandledErrorHandler:(void(^)(NSError *error))handler;
+ (void)addChainCompletionHandler:(dispatch_block_t)handler;
+ (void)setVerboseLoggingEnabled:(BOOL)enabled;
// init + fulfillUsingWorkBlock
+ (instancetype)promiseWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo workBlock:(MYPROJPromiseWorkBlock)workBlock;
+ (instancetype)promiseWithObject:(id)object; // can be MYPROJPromise, NSError or anything else
+ (instancetype)promiseWithResult:(ResultType)result;
+ (instancetype)failedPromiseWithError:(NSError *)error;
- (instancetype)initWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo;
- (instancetype)initWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo completed:(BOOL)completed result:(ResultType)result error:(NSError *)error NS_DESIGNATED_INITIALIZER;
// static data (can be invoked from any queue/thread)
@property (nonatomic, readonly) NSDictionary *userInfo;
// completion handlers (can be invoked from any queue/thread); return self for chaining
- (instancetype)whenSucceeded:(MYPROJPromiseResultBlock)handler;
- (instancetype)whenFailed:(MYPROJPromiseErrorBlock)handler;
- (instancetype)whenCompleted:(MYPROJPromiseResultAndErrorBlock)handler;
- (instancetype)whenDone:(dispatch_block_t)handler; // doesn't consume the error
// reading (can only be invoked AFTER the promise is fulfilled, i.e. after a completion handler has been called)
@property (nonatomic, readonly) BOOL completed;
@property (nonatomic, readonly) BOOL succeeded;
@property (nonatomic, readonly) BOOL failed;
@property (nonatomic, readonly) ResultType result;
@property (nonatomic, readonly) NSError *error;
// linking (can be invoked from any queue/thread)
- (void)whenFailedFailPromise:(MYPROJPromise *)linkedPromise;
- (void)whenCompletedFulfillPromise:(MYPROJPromise *)linkedPromise;
// promise transformation (can be invoked from any queue/thread; blocks can return MYPROJPromise, NSError or the actual result)
- (instancetype)newPromiseWithUserInfo:(NSDictionary *)userInfo;
- (MYPROJPromise *)promiseByTransformingResultUsingBlock:(id(^)(ResultType result))block;
- (MYPROJPromise *)promiseByWaitingForSuccessAndExecuting:(id(^)(void))block; // result is ignored
// combination
+ (instancetype)promiseByWaitingForAllRegardless:(NSArray *)promises;
+ (instancetype)promiseByWaitingForAllToSucceed:(NSArray *)promises;
// cancellation (can be invoked from any queue/thread)
- (void)cancel;
+ (BOOL)isCancelation:(NSError *)error;
// automatic fulfilment (sets NSProgress and runs the work block)
- (void)fulfillUsingWorkBlock:(MYPROJPromiseWorkBlock)workBlock;
// manual fulfilment (can be invoked from any queue/thread, extra calls are ignored)
- (void)didSucceedWithResult:(ResultType)result;
- (void)didFailWithError:(NSError *)error;
- (void)fulfillWithObject:(id)object; // MYPROJPromise, NSError or result
@property (nonatomic, readonly) MYPROJPromiseResultBlock fulfillWithResultBlock;
@property (nonatomic, readonly) MYPROJPromiseErrorBlock fulfillWithErrorBlock;
@property (nonatomic, readonly) MYPROJPromiseResultAndErrorBlock fulfillWithResultAndErrorBlock;
@property (nonatomic, readonly) MYPROJPromiseErrorAndResultBlock fulfillWithErrorAndResultBlock;
@property (nonatomic, readonly) MYPROJPromiseResultOrErrorBlock fulfillWithResultOrErrorBlock;
@end
#import "MYPROJPromise.h"
#include <libkern/OSAtomic.h>
typedef NS_ENUM(NSInteger, MYPROJPromiseState) {
MYPROJPromiseStateUnknown = 0,
MYPROJPromiseStateSucceeded,
MYPROJPromiseStateFailed,
};
NSString *MYPROJPromiseUserInfoDebugDescriptionKey = @"MYPROJPromise.debugDescription";
NSString *MYPROJPromiseUserInfoURLSessionDataTaskKey = @"MYPROJPromise.URLSessionDataTask";
NSString *MYPROJPromiseErrorDomain = @"MYPROJPromise";
static volatile BOOL g_verboseLoggingEnabled;
@implementation MYPROJPromise {
dispatch_queue_t _queue;
NSProgress *_progress;
// writes are on _queue
volatile MYPROJPromiseState _state;
volatile id _result;
NSError *volatile _error;
NSMutableArray *_completionHandlers;
BOOL _consumedByChain;
BOOL _failureConsumed;
}
#pragma mark - Global
static void(^g_errorHandler)(NSError *error);
static dispatch_block_t g_chainCompletionHandler;
+ (void)setUnhandledErrorHandler:(void(^)(NSError *error))handler {
g_errorHandler = handler;
}
+ (void)addChainCompletionHandler:(dispatch_block_t)handler {
dispatch_block_t prev = g_chainCompletionHandler;
if (prev != nil) {
dispatch_block_t original = handler;
handler = ^{
prev();
original();
};
}
g_chainCompletionHandler = handler;
}
#pragma mark - Lifecycle
//+ (instancetype)promiseWithSerialQueue:(dispatch_queue_t)queue object:(id)object {
// if ([object isKindOfClass:[MYPROJPromise class]]) {
// return object;
// } else if ([object isKindOfClass:[NSError class]]) {
// return [[MYPROJPromise alloc] initWithSerialQueue:_queue userInfo:nil completed:YES result:nil error:object];
// } else {
// return [[MYPROJPromise alloc] initWithSerialQueue:_queue userInfo:nil completed:YES result:object error:nil];
// }
//}
+ (instancetype)promiseWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo workBlock:(MYPROJPromiseWorkBlock)workBlock {
MYPROJPromise *promise = [[MYPROJPromise alloc] initWithSerialQueue:queue userInfo:userInfo];
[promise fulfillUsingWorkBlock:workBlock];
return promise;
}
+ (instancetype)failedPromiseWithError:(NSError *)error {
NSParameterAssert([error isKindOfClass:[NSError class]]);
return [[self alloc] initWithSerialQueue:dispatch_get_main_queue() userInfo:nil completed:YES result:nil error:error];
}
+ (instancetype)promiseWithResult:(id)result {
return [[self alloc] initWithSerialQueue:dispatch_get_main_queue() userInfo:nil completed:YES result:result error:nil];
}
+ (instancetype)promiseWithObject:(id)object {
if ([object isKindOfClass:[MYPROJPromise class]]) {
return object;
} else if ([object isKindOfClass:[NSError class]]) {
return [MYPROJPromise failedPromiseWithError:object];
} else {
return [MYPROJPromise promiseWithResult:object];
}
}
- (instancetype)initWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo {
return [self initWithSerialQueue:queue userInfo:userInfo completed:NO result:nil error:nil];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not implemented" userInfo:nil];
}
#pragma clang diagnostic pop
- (instancetype)initWithSerialQueue:(dispatch_queue_t)queue userInfo:(NSDictionary *)userInfo completed:(BOOL)completed result:(id)result error:(NSError *)error
{
self = [super init];
if (self) {
_queue = queue ?: dispatch_get_main_queue();
_userInfo = [userInfo copy] ?: @{};
if (completed) {
if (error) {
NSParameterAssert([error isKindOfClass:[NSError class]]);
_state = MYPROJPromiseStateFailed;
_error = error;
} else {
_state = MYPROJPromiseStateSucceeded;
_result = result;
}
} else {
_progress = [NSProgress progressWithTotalUnitCount:1];
}
}
return self;
}
#pragma mark - Execution
- (void)fulfillUsingWorkBlock:(MYPROJPromiseWorkBlock)workBlock {
dispatch_async(_queue, ^{
[_progress becomeCurrentWithPendingUnitCount:1];
workBlock(^(id result, NSError *error) {
[self _didCompleteWithResult:result orError:error];
});
[_progress resignCurrent];
});
}
#pragma mark - Debugging
+ (void)setVerboseLoggingEnabled:(BOOL)enabled {
g_verboseLoggingEnabled = enabled;
}
- (NSString *)description {
return [self debugDescription];
}
- (NSString *)debugDescription {
NSString *stateDescription = [self stateDescription];
NSString *comment = _userInfo[MYPROJPromiseUserInfoDebugDescriptionKey];
if (comment) {
return [NSString stringWithFormat:@"P(%@ %@)", stateDescription, comment];
} else {
return [NSString stringWithFormat:@"P(%@)", stateDescription];
}
}
- (NSString *)stateDescription {
switch (_state) {
case MYPROJPromiseStateUnknown: return @"?";
case MYPROJPromiseStateSucceeded: return [NSString stringWithFormat:@"r=%@", _result];
case MYPROJPromiseStateFailed: return [NSString stringWithFormat:@"e=%@", _error.debugDescription];
}
abort();
}
#pragma mark - Consumption
- (void)setConsumedByChain {
_consumedByChain = YES;
}
#pragma mark - Completion handlers
- (instancetype)whenSucceeded:(MYPROJPromiseResultBlock)handler {
[self _whenCompleted:^(id result, NSError *error) {
if (error == nil) {
handler(result);
}
}];
return self;
}
- (instancetype)whenFailed:(MYPROJPromiseErrorBlock)handler {
_failureConsumed = YES;
[self _whenCompleted:^(id result, NSError *error) {
if (error != nil) {
handler(error);
}
}];
return self;
}
- (instancetype)whenCompleted:(MYPROJPromiseResultAndErrorBlock)handler {
_failureConsumed = YES;
return [self _whenCompleted:handler];
}
- (instancetype)whenDone:(dispatch_block_t)handler {
return [self _whenCompleted:^(id result, NSError *error) {
handler();
}];
}
- (instancetype)_whenCompleted:(MYPROJPromiseResultAndErrorBlock)handler {
dispatch_async(_queue, ^{
if (_state == MYPROJPromiseStateUnknown) {
if (_completionHandlers == nil) {
_completionHandlers = [NSMutableArray new];
}
[_completionHandlers addObject:[handler copy]];
} else {
handler(_result, _error);
}
});
return self;
}
#pragma mark - Reading
- (BOOL)succeeded {
return _state == MYPROJPromiseStateSucceeded;
}
- (BOOL)failed {
return _state == MYPROJPromiseStateFailed;
}
- (BOOL)completed {
return _state != MYPROJPromiseStateUnknown;
}
- (id)result {
return _result;
}
- (NSError *)error {
return _error;
}
#pragma mark - Linking
- (void)whenFailedFailPromise:(MYPROJPromise *)linkedPromise {
[self whenFailed:^(NSError *error) {
[linkedPromise didFailWithError:error];
}];
}
- (void)whenCompletedFulfillPromise:(MYPROJPromise *)linkedPromise {
[self whenCompleted:^(id result, NSError *error) {
[linkedPromise _didCompleteWithResult:result orError:error];
}];
}
#pragma mark - Transformation
- (instancetype)newPromiseWithUserInfo:(NSDictionary *)userInfo {
return [[MYPROJPromise alloc] initWithSerialQueue:_queue userInfo:userInfo];
}
- (MYPROJPromise *)promiseByTransformingResultUsingBlock:(id(^)(id result))block {
MYPROJPromise *promise = [self newPromiseWithUserInfo:nil];
[self setConsumedByChain];
[self whenCompleted:^(id result, NSError *error) {
if (error) {
[promise didFailWithError:error];
} else {
id object = block(result);
[promise fulfillWithObject:object];
}
}];
return promise;
}
- (MYPROJPromise *)promiseByWaitingForSuccessAndExecuting:(id(^)(void))block {
MYPROJPromise *promise = [self newPromiseWithUserInfo:nil];
[self setConsumedByChain];
[self whenCompleted:^(id result, NSError *error) {
if (error) {
[promise didFailWithError:error];
} else {
id object = block();
[promise fulfillWithObject:object];
}
}];
return promise;
}
#pragma mark - Combination
+ (instancetype)promiseByCombiningPromises:(NSArray *)promises usingBlock:(void(^)(MYPROJPromise *first, MYPROJPromise *second, MYPROJPromise *combined))combinatorBlock {
switch (promises.count) {
case 0:
return [MYPROJPromise promiseWithResult:nil];
case 1:
return [promises firstObject];
case 2: {
MYPROJPromise *first = promises[0];
MYPROJPromise *second = promises[1];
[first setConsumedByChain];
[second setConsumedByChain];
MYPROJPromise *combined = [first newPromiseWithUserInfo:nil];
combinatorBlock(first, second, combined);
return combined;
}
default: {
MYPROJPromise *first = [self promiseByCombiningPromises:[promises subarrayWithRange:NSMakeRange(0, promises.count - 1)] usingBlock:combinatorBlock];
MYPROJPromise *last = [promises lastObject];
MYPROJPromise *combined = [first newPromiseWithUserInfo:nil];
combinatorBlock(first, last, combined);
return combined;
}
}
}
+ (instancetype)promiseByWaitingForAllRegardless:(NSArray *)promises {
return [self promiseByCombiningPromises:promises usingBlock:^(MYPROJPromise *first, MYPROJPromise *second, MYPROJPromise *combined) {
[first whenCompleted:^(id result1, NSError *error1) {
[second whenCompleted:^(id result2, NSError *error2) {
if (error1) {
[combined didFailWithError:error1];
} else if (error2) {
[combined didFailWithError:error2];
} else {
[combined didSucceedWithResult:nil];
}
}];
}];
}];
}
+ (instancetype)promiseByWaitingForAllToSucceed:(NSArray *)promises {
return [self promiseByCombiningPromises:promises usingBlock:^(MYPROJPromise *first, MYPROJPromise *second, MYPROJPromise *combined) {
[first whenFailedFailPromise:combined];
[second whenFailedFailPromise:combined];
[first whenSucceeded:^(id result1) {
[second whenSucceeded:^(id result2) {
[combined didSucceedWithResult:nil];
}];
}];
}];
}
#pragma mark - Progress and cancelation
- (void)cancel {
[_progress cancel];
[self didFailWithError:[NSError errorWithDomain:MYPROJPromiseErrorDomain code:MYPROJPromiseErrorCodeCanceled userInfo:@{}]];
}
+ (BOOL)isCancelation:(NSError *)error {
if ([error.domain isEqualToString:MYPROJPromiseErrorDomain]) {
return error.code == MYPROJPromiseErrorCodeCanceled;
} else if ([error.domain isEqualToString:NSCocoaErrorDomain]) {
return error.code == NSUserCancelledError;
} else {
return NO;
}
}
#pragma mark - Fulfilment
- (void)didSucceedWithResult:(id)result {
dispatch_async(_queue, ^{
if (_state == MYPROJPromiseStateUnknown) {
_state = MYPROJPromiseStateSucceeded;
_result = result;
[self _executeCompletionHandlers];
} else {
if (g_verboseLoggingEnabled) {
if (_state == MYPROJPromiseStateSucceeded) {
NSLog(@"MYPROJPromise: Ignoring attempt to set result on an already succeeded promise");
} else {
NSLog(@"MYPROJPromise: Ignoring attempt to set result on an already failed promise");
}
}
}
});
}
- (void)didFailWithError:(NSError *)error {
NSParameterAssert(error != nil);
NSParameterAssert([error isKindOfClass:[NSError class]]);
dispatch_async(_queue, ^{
if (_state == MYPROJPromiseStateUnknown) {
_state = MYPROJPromiseStateFailed;
_error = error;
[self _executeCompletionHandlers];
} else {
if (g_verboseLoggingEnabled) {
if (_state == MYPROJPromiseStateSucceeded) {
NSLog(@"MYPROJPromise: Ignoring attempt to set error on an already succeeded promise");
} else {
NSLog(@"MYPROJPromise: Ignoring attempt to set error on an already failed promise");
}
}
}
});
}
- (void)fulfillWithObject:(id)object {
if ([object isKindOfClass:[MYPROJPromise class]]) {
[(MYPROJPromise *)object whenCompletedFulfillPromise:self];
} else if ([object isKindOfClass:[NSError class]]) {
[self didFailWithError:object];
} else {
[self didSucceedWithResult:object];
}
}
- (void)_didCompleteWithResult:(id)result orError:(NSError *)error {
if (error) {
[self didFailWithError:error];
} else {
[self didSucceedWithResult:result];
}
}
- (void)_executeCompletionHandlers {
id result = _result;
NSError *error = _error;
for (MYPROJPromiseResultAndErrorBlock handler in _completionHandlers) {
handler(result, error);
}
_completionHandlers = nil;
if (!_failureConsumed && _error) {
g_errorHandler(_error);
}
if (!_consumedByChain) {
g_chainCompletionHandler();
}
}
#pragma mark - Fulfillment blocks
- (MYPROJPromiseResultBlock)fulfillWithResultBlock {
return ^(id result) { [self didSucceedWithResult:result]; };
}
- (MYPROJPromiseErrorBlock)fulfillWithErrorBlock {
return ^(NSError *error) {
if (error) {
[self didFailWithError:error];
} else {
[self didSucceedWithResult:nil];
}
};
}
- (MYPROJPromiseResultAndErrorBlock)fulfillWithResultAndErrorBlock {
return ^(id result, NSError *error) {
if (error) {
[self didFailWithError:error];
} else {
[self didSucceedWithResult:result];
}
};
}
- (MYPROJPromiseErrorAndResultBlock)fulfillWithErrorAndResultBlock {
return ^(NSError *error, id result) {
if (error) {
[self didFailWithError:error];
} else {
[self didSucceedWithResult:result];
}
};
}
- (MYPROJPromiseResultOrErrorBlock)fulfillWithResultOrErrorBlock {
return ^(id resultOrError) { [self fulfillWithObject:resultOrError]; };
}
@end
@import Foundation;
#import "MYPROJPromise.h"
NS_ASSUME_NONNULL_BEGIN
extern NSString *const MYPROJServerErrorDomain;
typedef NS_ENUM(NSInteger, MYPROJServerErrorCode) {
MYPROJServerErrorNone,
MYPROJServerErrorNetworkRelated,
MYPROJServerErrorGeneralCommunicationFailure,
MYPROJServerErrorReauthenticationRequired,
MYPROJServerErrorNotAuthenticated,
MYPROJServerErrorUnknown,
};
typedef NS_ENUM(NSInteger, MYPROJDataType) {
MYPROJDataTypeAutoDetect = -1,
MYPROJDataTypeNone = 0,
MYPROJDataTypeJSON,
MYPROJDataTypeImage,
MYPROJDataTypeBinary,
MYPROJDataTypeString,
};
@interface MYPROJServer : NSObject
+ (instancetype)sharedServer;
- (MYPROJPromise<MYPROJSomething *> *)doSomething:(NSString *)arg;
- (void)handleStartup;
@end
NS_ASSUME_NONNULL_END
#import "MYPROJServer.h"
#import "MYPROJPreferences.h"
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
static NSString *MYPROJURLEncode(NSDictionary *_Nullable values);
NSString *const MYPROJServerErrorDomain = @"com.MYPROJ.ServerAPI";
NSString *const MYPROJServerErrorInfoHTTPStatusCode = @"HTTPStatusCode";
NSString *const MYPROJServerErrorInfoResponseText = @"responseText";
NSString *const MYPROJServerErrorInfoResponseObject = @"responseObject";
NSString *const MYPROJServerErrorInfoIdentifier = @"errid";
@implementation MYPROJServer {
NSURLSession *_session;
NSInteger _verbosity;
}
static MYPROJServer *sharedServer;
+ (instancetype)sharedServer {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedServer = [MYPROJServer new];
});
return sharedServer;
}
- (instancetype)init {
self = [super init];
if (self) {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.HTTPMaximumConnectionsPerHost = 1;
configuration.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
configuration.URLCache = nil;
configuration.HTTPAdditionalHeaders = @{};
_session = [NSURLSession sessionWithConfiguration:configuration];
}
return self;
}
- (NSURL *)baseURL {
return [NSURL URLWithString:[MYPROJPreferences sharedPreferences].syncServerEndpoint];
}
#pragma mark - API calls
- (MYPROJPromise<MYPROJSomething *> *)doSomething:(NSString *)arg {
return [self performRequestWithMethod:.....];
}
#pragma mark - HTTP Helpers
- (MYPROJPromise *)performRequestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *_Nullable)parameters body:(id _Nullable)body extraHeaders:(NSDictionary<NSString *, NSString *> *_Nullable)extraHeaders responseType:(MYPROJDataType)responseType {
MYPROJPromise *promise = [[MYPROJPromise alloc] initWithSerialQueue:dispatch_get_main_queue() userInfo:nil];
BOOL hasBody = ([method isEqualToString:@"POST"] || [method isEqualToString:@"PUT"]);
NSURL *url;
if (hasBody) {
url = [NSURL URLWithString:path relativeToURL:self.baseURL];
NSParameterAssert(!(body != nil && parameters != nil));
} else {
url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?%@", path, MYPROJURLEncode(parameters)] relativeToURL:self.baseURL];
NSParameterAssert(body == nil);
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = method;
if (hasBody) {
if (body) {
request.HTTPBody = body;
} else {
request.HTTPBody = [MYPROJURLEncode(parameters) dataUsingEncoding:NSUTF8StringEncoding];
[request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
}
[extraHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull obj, BOOL * _Nonnull stop) {
[request setValue:obj forHTTPHeaderField:key];
}];
NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate];
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSTimeInterval elapsedTime = [NSDate timeIntervalSinceReferenceDate] - startTime;
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
if (error) {
NSError *serverError = [NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorNetworkRelated userInfo:@{NSUnderlyingErrorKey: error}];
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) => [%ld] network error: %@", request.HTTPMethod, request.URL.absoluteString, elapsedTime, httpResponse.statusCode, error.localizedDescription);
[promise didFailWithError:serverError];
return;
}
NSString *mimeType = httpResponse.MIMEType;
BOOL isFailure = (httpResponse.statusCode < 200 || (httpResponse.statusCode >= 300 && httpResponse.statusCode != 304));
[self decodeData:data forMimeType:mimeType responseType:(isFailure ? MYPROJDataTypeAutoDetect : responseType) completionHandler:^(NSError *decodingError, id responseObject) {
if (isFailure) {
if (decodingError != nil) {
NSString *responseText = @"";
if (data) {
// hopefully they're not serving binary files as error responses
// (although I can see a sad kitty picture being appropriate in some cases)
responseText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
NSError *serverError = [NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorGeneralCommunicationFailure userInfo:@{MYPROJServerErrorInfoHTTPStatusCode: @(httpResponse.statusCode), MYPROJServerErrorInfoResponseText: responseText}];
if (_verbosity >= 2) {
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) %@ %@ => !!! %d, error decoding response: %@, raw response text:\n%@<end of response>\n", request.HTTPMethod, request.URL.absoluteString, elapsedTime, parameters, request.allHTTPHeaderFields, (int)httpResponse.statusCode, decodingError.debugDescription, responseText);
} else {
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) => !!! %d, error decoding response: %@", request.HTTPMethod, [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteString, elapsedTime, (int)httpResponse.statusCode, decodingError);
}
[promise didFailWithError:serverError];
} else {
NSError *serverError = [self errorForServerErrorReponse:responseObject statusCode:httpResponse.statusCode];
if (serverError == nil) {
NSDictionary *userInfo;
if (responseObject) {
userInfo = @{MYPROJServerErrorInfoHTTPStatusCode: @(httpResponse.statusCode), MYPROJServerErrorInfoResponseObject: responseObject};
} else {
userInfo = @{MYPROJServerErrorInfoHTTPStatusCode: @(httpResponse.statusCode)};
}
serverError = [NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorGeneralCommunicationFailure userInfo:userInfo];
}
// if (_verbosity >= 2) {
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) %@ %@ => !!! %d %@", request.HTTPMethod, request.URL.absoluteString, elapsedTime, parameters, request.allHTTPHeaderFields, (int)httpResponse.statusCode, responseObject);
// } else {
// NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) => !!! %d", request.HTTPMethod, [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteString, elapsedTime, (int)httpResponse.statusCode);
// }
[promise didFailWithError:serverError];
}
return;
}
if (decodingError) {
NSString *responseText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *serverError = [NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorGeneralCommunicationFailure userInfo:@{MYPROJServerErrorInfoHTTPStatusCode: @(httpResponse.statusCode), MYPROJServerErrorInfoResponseText: responseText, NSUnderlyingErrorKey: decodingError}];
if (_verbosity >= 2) {
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) %@ %@ => %d, error decoding response: %@, raw response text:\n%@<end of response>\n", request.HTTPMethod, request.URL.absoluteString, elapsedTime, parameters, request.allHTTPHeaderFields, (int)httpResponse.statusCode, decodingError.debugDescription, responseText);
} else {
NSLog(@"MYPROJ: WARNING: ServerAPI: HTTP %@ %@ (%.1lfs) => %d, error decoding response: %@", request.HTTPMethod, [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteString, elapsedTime, (int)httpResponse.statusCode, decodingError);
}
[promise didFailWithError:serverError];
return;
}
if (_verbosity >= 2) {
NSLog(@"MYPROJ: ServerAPI: HTTP %@ %@ (%.1lfs) %@ %@ => %d %@", request.HTTPMethod, request.URL.absoluteString, elapsedTime, parameters, request.allHTTPHeaderFields, (int)httpResponse.statusCode, responseObject);
} else if (_verbosity >= 1) {
NSLog(@"MYPROJ: ServerAPI: HTTP %@ %@ (%.1lfs) => %d", request.HTTPMethod, [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteString, elapsedTime, (int)httpResponse.statusCode);
}
[promise didSucceedWithResult:responseObject];
}];
}];
[task resume];
return promise;
}
- (void)decodeData:(NSData *)data forMimeType:(NSString *)mime responseType:(MYPROJDataType)responseType completionHandler:(void(^)(NSError *_Nullable error, id _Nullable responseObject))completionHandler {
if (responseType == MYPROJDataTypeAutoDetect) {
if ([mime isEqualToString:@"application/json"] || [mime isEqualToString:@"text/json"]) {
responseType = MYPROJDataTypeJSON;
} else if ([mime isEqualToString:@"text/plain"] || [mime isEqualToString:@"text/html"]) {
responseType = MYPROJDataTypeString;
} else if (data.length == 0) {
responseType = MYPROJDataTypeNone;
} else {
uint8_t first = ((const uint8_t *)data.bytes)[0];
if (first == '[' || first == '{') {
responseType = MYPROJDataTypeJSON;
} else {
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (string) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, string);
});
} else {
responseType = MYPROJDataTypeBinary;
}
}
}
}
if (responseType == MYPROJDataTypeJSON) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
id responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(error, responseObject);
});
});
} else if (responseType == MYPROJDataTypeString) {
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
dispatch_async(dispatch_get_main_queue(), ^{
if (string) {
completionHandler(nil, string);
} else {
completionHandler([NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorGeneralCommunicationFailure userInfo:nil], nil);
}
});
} else if (responseType == MYPROJDataTypeImage) {
NSImage *image = [[NSImage alloc] initWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionHandler(nil, image);
} else {
completionHandler([NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorGeneralCommunicationFailure userInfo:nil], nil);
}
});
} else if (responseType == MYPROJDataTypeBinary) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, data);
});
} else if (responseType == MYPROJDataTypeNone) {
completionHandler(nil, nil);
} else {
abort();
}
}
- (NSError *_Nullable)errorForServerErrorReponse:(id)responseObject statusCode:(NSInteger)statusCode {
if (statusCode == 401) {
return [NSError errorWithDomain:MYPROJServerErrorDomain code:MYPROJServerErrorReauthenticationRequired userInfo:@{MYPROJServerErrorInfoHTTPStatusCode: @(statusCode), MYPROJServerErrorInfoResponseObject: responseObject}];
}
return nil;
}
@end
static NSString *MYPROJURLEncode(NSDictionary *_Nullable values) {
static NSCharacterSet *allowed;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[cs removeCharactersInString:@"?&=@+/"];
allowed = [cs copy];
});
if (values.count == 0) {
return @"";
}
NSMutableArray *pairs = [NSMutableArray new];
[values enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSString *encodedKey = [[key description] stringByAddingPercentEncodingWithAllowedCharacters:allowed];
NSString *encodedValue = [[obj description] stringByAddingPercentEncodingWithAllowedCharacters:allowed];
[pairs addObject:[NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue]];
}];
return [pairs componentsJoinedByString:@"&"];
}
NS_ASSUME_NONNULL_END
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment