Created
December 25, 2017 10:45
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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