Skip to content

Commit

Permalink
Merge branch 'feature/instance'
Browse files Browse the repository at this point in the history
  • Loading branch information
ishkawa committed Mar 9, 2015
2 parents 653bfa1 + b006e77 commit 1d35ef8
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
path = Carthage/Checkouts/LlamaKit
url = https://github.com/LlamaKit/LlamaKit.git
[submodule "Carthage/Checkouts/Assertions"]
path = Carthage/Checkouts/Assertions
url = https://github.com/ikesyo/Assertions.git
[submodule "Carthage/Checkouts/OHHTTPStubs"]
path = Carthage/Checkouts/OHHTTPStubs
url = https://github.com/ishkawa/OHHTTPStubs.git
20 changes: 20 additions & 0 deletions APIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
7F0869A61A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; };
7F0869A71A978BCA001AD3E1 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */; };
7F0869A81A979088001AD3E1 /* APIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* APIKit.swift */; };
7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; };
7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */; };
7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */; };
7F45FD181A94D085006863BB /* APIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD171A94D085006863BB /* APIKit.swift */; };
7F45FD421A94D1CC006863BB /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
Expand All @@ -30,10 +32,14 @@
7F45FD711A94DA2B006863BB /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
7F45FD721A94DA2B006863BB /* LlamaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7F45FD741A94E832006863BB /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F45FD731A94E832006863BB /* Models.swift */; };
7FAC25A01AA2C03400E92500 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */; };
7FAC25A11AA2C04000E92500 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */; };
7FAC25A21AA2C1D500E92500 /* OHHTTPStubs.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7FCBE9DD1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; };
7FCBE9DE1A9734880075AFD9 /* RequestBodyBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */; };
7FCBE9E01A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; };
7FCBE9E11A9734950075AFD9 /* ResponseBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */; };
7FD65B141AA306BB008DCA2C /* OHHTTPStubs.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */; };
7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FCDD1A94D02C006863BB /* APIKit.framework */; };
7FEC5A211A96FFD300B1D3C0 /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */; };
Expand Down Expand Up @@ -77,6 +83,7 @@
files = (
CDB0CCDF1A9B2F6700BADAC5 /* Assertions.framework in CopyFiles */,
7F0869A41A9787E3001AD3E1 /* LlamaKit.framework in CopyFiles */,
7FD65B141AA306BB008DCA2C /* OHHTTPStubs.framework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -100,6 +107,7 @@
files = (
CDB0CCDC1A9B2ED600BADAC5 /* Assertions.framework in CopyFiles */,
7FEC5A231A97001D00B1D3C0 /* LlamaKit.framework in CopyFiles */,
7FAC25A21AA2C1D500E92500 /* OHHTTPStubs.framework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -108,6 +116,7 @@
/* Begin PBXFileReference section */
7F0869941A978790001AD3E1 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7F0869A51A978BCA001AD3E1 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedSerialization.swift; sourceTree = "<group>"; };
7F1B190A1AA2CA1300C7AFCF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = "<group>"; };
7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = "<group>"; };
7F45FCDD1A94D02C006863BB /* APIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = APIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7F45FCE11A94D02C006863BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -125,6 +134,8 @@
7F45FD561A94D9A9006863BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
7F45FD6A1A94D9F9006863BB /* GitHub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHub.swift; sourceTree = "<group>"; };
7F45FD731A94E832006863BB /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = "<group>"; };
7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = "<group>"; };
7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = "<group>"; };
7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParser.swift; sourceTree = "<group>"; };
7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -140,6 +151,7 @@
buildActionMask = 2147483647;
files = (
7F08699A1A978790001AD3E1 /* APIKit.framework in Frameworks */,
7FAC25A01AA2C03400E92500 /* OHHTTPStubs.framework in Frameworks */,
CDB0CCDE1A9B2F6100BADAC5 /* Assertions.framework in Frameworks */,
7F0869A31A9787E1001AD3E1 /* LlamaKit.framework in Frameworks */,
);
Expand Down Expand Up @@ -175,6 +187,7 @@
buildActionMask = 2147483647;
files = (
7FEC5A1A1A96FE2600B1D3C0 /* APIKit.framework in Frameworks */,
7FAC25A11AA2C04000E92500 /* OHHTTPStubs.framework in Frameworks */,
CDB0CCDB1A9B2ECD00BADAC5 /* Assertions.framework in Frameworks */,
7FEC5A211A96FFD300B1D3C0 /* LlamaKit.framework in Frameworks */,
);
Expand Down Expand Up @@ -247,6 +260,7 @@
7F45FD1B1A94D1B4006863BB /* iOS */ = {
isa = PBXGroup;
children = (
7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */,
CDB0CCDA1A9B2ECD00BADAC5 /* Assertions.framework */,
7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */,
);
Expand All @@ -256,6 +270,7 @@
7F45FD1D1A94D1B4006863BB /* Mac */ = {
isa = PBXGroup;
children = (
7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */,
CDB0CCDD1A9B2F6100BADAC5 /* Assertions.framework */,
7F45FD1E1A94D1B4006863BB /* LlamaKit.framework */,
);
Expand Down Expand Up @@ -288,6 +303,7 @@
7FEC5A151A96FE2600B1D3C0 /* APIKitTests */ = {
isa = PBXGroup;
children = (
7F1B190A1AA2CA1300C7AFCF /* APITests.swift */,
7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */,
7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */,
7FEC5A161A96FE2600B1D3C0 /* Supporting Files */,
Expand Down Expand Up @@ -510,6 +526,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */,
7F0869A01A9787AF001AD3E1 /* RequestBodyBuilderTests.swift in Sources */,
7F0869A11A9787AF001AD3E1 /* ResponseBodyParserTests.swift in Sources */,
);
Expand Down Expand Up @@ -552,6 +569,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */,
7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */,
7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */,
);
Expand Down Expand Up @@ -734,6 +752,7 @@
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
INFOPLIST_FILE = APIKit/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand All @@ -756,6 +775,7 @@
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
INFOPLIST_FILE = APIKit/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand Down
131 changes: 112 additions & 19 deletions APIKit/APIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,94 @@ public enum Method: String {
case CONNECT = "CONNECT"
}

public class API {
// configurations
public class func baseURL() -> NSURL {
return NSURL()
}
private var dataTaskResponseBufferKey = 0
private var dataTaskCompletionHandlerKey = 0

public class func URLSession() -> NSURLSession {
return NSURLSession.sharedSession()
private extension NSURLSessionDataTask {
private var responseBuffer: NSMutableData {
if let responseBuffer = objc_getAssociatedObject(self, &dataTaskResponseBufferKey) as? NSMutableData {
return responseBuffer
} else {
let responseBuffer = NSMutableData()
objc_setAssociatedObject(self, &dataTaskResponseBufferKey, responseBuffer, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
return responseBuffer
}
}

private var completionHandler: ((NSData?, NSURLResponse?, NSError?) -> Void)? {
get {
return (objc_getAssociatedObject(self, &dataTaskCompletionHandlerKey) as? Box<(NSData?, NSURLResponse?, NSError?) -> Void>)?.unbox
}

set {
if let value = newValue {
objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, Box(value), UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
} else {
objc_setAssociatedObject(self, &dataTaskCompletionHandlerKey, nil, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
}
}
}
}

// use private, global scope variable until we can use stored class var in Swift 1.2
private var instancePairDictionary = [String: (API, NSURLSession)]()
private let instancePairSemaphore = dispatch_semaphore_create(1)

public class API: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate {
// configurations
public class func baseURL() -> NSURL {
fatalError("API.baseURL() must be overrided in subclasses.")
}

public class func requestBodyBuilder() -> RequestBodyBuilder {
return .JSON(writingOptions: nil)
}

public class func responseBodyParser() -> ResponseBodyParser {
return .JSON(readingOptions: nil)
}

public class func URLSessionConfiguration() -> NSURLSessionConfiguration {
return NSURLSessionConfiguration.defaultSessionConfiguration()
}

public class func URLSessionDelegateQueue() -> NSOperationQueue? {
// nil indicates NSURLSession creates its own serial operation queue.
// see doc of NSURLSession.init(configuration:delegate:delegateQueue:) for more details.
return nil
}

// prevent instantiation
override private init() {
super.init()
}

// create session and instance of API for each subclasses
private final class var instancePair: (API, NSURLSession) {
let className = NSStringFromClass(self)

dispatch_semaphore_wait(instancePairSemaphore, DISPATCH_TIME_FOREVER)
let pair: (API, NSURLSession) = instancePairDictionary[className] ?? {
let instance = (self as NSObject.Type)() as API
let configuration = self.URLSessionConfiguration()
let queue = self.URLSessionDelegateQueue()
let session = NSURLSession(configuration: configuration, delegate: instance, delegateQueue: queue)
let pair = (instance, session)
instancePairDictionary[className] = pair
return pair
}()
dispatch_semaphore_signal(instancePairSemaphore)

return pair
}

public final class var instance: API {
return instancePair.0
}

public final class var URLSession: NSURLSession {
return instancePair.1
}

// build NSURLRequest
public class func URLRequest(method: Method, _ path: String, _ parameters: [String: AnyObject] = [:]) -> NSURLRequest? {
Expand Down Expand Up @@ -77,11 +148,13 @@ public class API {

// send request and build response object
public class func sendRequest<T: Request>(request: T, handler: (Result<T.Response, NSError>) -> Void = {r in}) -> NSURLSessionDataTask? {
let session = URLSession()
let session = URLSession
let mainQueue = dispatch_get_main_queue()

if let URLRequest = request.URLRequest {
let task = session.dataTaskWithRequest(URLRequest) { data, URLResponse, connectionError in
let task = session.dataTaskWithRequest(URLRequest)

task.completionHandler = { data, URLResponse, connectionError in
if let error = connectionError {
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
return
Expand All @@ -94,18 +167,24 @@ public class API {
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
return
}

let mappedResponse: Result<T.Response, NSError> = self.responseBodyParser().parseData(data).flatMap { rawResponse in
if let response = request.responseFromObject(rawResponse) {
return success(response)
} else {
let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."]
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
return failure(error)

if let data = data {
let mappedResponse: Result<T.Response, NSError> = self.responseBodyParser().parseData(data).flatMap { rawResponse in
if let response = request.responseFromObject(rawResponse) {
return success(response)
} else {
let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."]
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
return failure(error)
}

}

dispatch_async(mainQueue, { handler(mappedResponse) })
} else {
let userInfo = [NSLocalizedDescriptionKey: "unable to get response body despite NSURLSession raised no error."]
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
}
dispatch_async(mainQueue, { handler(mappedResponse) })
}

task.resume()
Expand All @@ -119,4 +198,18 @@ public class API {
return nil
}
}

// MARK: - NSURLSessionTaskDelegate
// TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift.
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError connectionError: NSError?) {
if let dataTask = task as? NSURLSessionDataTask {
dataTask.completionHandler?(dataTask.responseBuffer, dataTask.response, connectionError)
}
}

// MARK: - NSURLSessionDataDelegate
// TODO: add attributes like NS_REQUIRES_SUPER when it is available in future version of Swift.
public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
dataTask.responseBuffer.appendData(data)
}
}
Loading

0 comments on commit 1d35ef8

Please sign in to comment.