diff --git a/.gitmodules b/.gitmodules index 9b182f5b..b623bd0e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 8e828704..f25c2cd1 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -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 */; }; @@ -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 */; }; @@ -77,6 +83,7 @@ files = ( CDB0CCDF1A9B2F6700BADAC5 /* Assertions.framework in CopyFiles */, 7F0869A41A9787E3001AD3E1 /* LlamaKit.framework in CopyFiles */, + 7FD65B141AA306BB008DCA2C /* OHHTTPStubs.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -100,6 +107,7 @@ files = ( CDB0CCDC1A9B2ED600BADAC5 /* Assertions.framework in CopyFiles */, 7FEC5A231A97001D00B1D3C0 /* LlamaKit.framework in CopyFiles */, + 7FAC25A21AA2C1D500E92500 /* OHHTTPStubs.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -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 = ""; }; + 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = ""; }; 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilderTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -125,6 +134,8 @@ 7F45FD561A94D9A9006863BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 7F45FD6A1A94D9F9006863BB /* GitHub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHub.swift; sourceTree = ""; }; 7F45FD731A94E832006863BB /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = ""; }; + 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OHHTTPStubs.framework; sourceTree = ""; }; 7FCBE9DC1A9734880075AFD9 /* RequestBodyBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBodyBuilder.swift; sourceTree = ""; }; 7FCBE9DF1A9734950075AFD9 /* ResponseBodyParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseBodyParser.swift; sourceTree = ""; }; 7FEC5A141A96FE2600B1D3C0 /* APIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -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 */, ); @@ -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 */, ); @@ -247,6 +260,7 @@ 7F45FD1B1A94D1B4006863BB /* iOS */ = { isa = PBXGroup; children = ( + 7FAC259C1AA2C00600E92500 /* OHHTTPStubs.framework */, CDB0CCDA1A9B2ECD00BADAC5 /* Assertions.framework */, 7F45FD1C1A94D1B4006863BB /* LlamaKit.framework */, ); @@ -256,6 +270,7 @@ 7F45FD1D1A94D1B4006863BB /* Mac */ = { isa = PBXGroup; children = ( + 7FAC259E1AA2C01100E92500 /* OHHTTPStubs.framework */, CDB0CCDD1A9B2F6100BADAC5 /* Assertions.framework */, 7F45FD1E1A94D1B4006863BB /* LlamaKit.framework */, ); @@ -288,6 +303,7 @@ 7FEC5A151A96FE2600B1D3C0 /* APIKitTests */ = { isa = PBXGroup; children = ( + 7F1B190A1AA2CA1300C7AFCF /* APITests.swift */, 7F30A8551A975BD600A8C136 /* RequestBodyBuilderTests.swift */, 7FEC5A181A96FE2600B1D3C0 /* ResponseBodyParserTests.swift */, 7FEC5A161A96FE2600B1D3C0 /* Supporting Files */, @@ -510,6 +526,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7F1B190C1AA2CA1300C7AFCF /* APITests.swift in Sources */, 7F0869A01A9787AF001AD3E1 /* RequestBodyBuilderTests.swift in Sources */, 7F0869A11A9787AF001AD3E1 /* ResponseBodyParserTests.swift in Sources */, ); @@ -552,6 +569,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7F1B190B1AA2CA1300C7AFCF /* APITests.swift in Sources */, 7FEC5A191A96FE2600B1D3C0 /* ResponseBodyParserTests.swift in Sources */, 7F30A8561A975BD600A8C136 /* RequestBodyBuilderTests.swift in Sources */, ); @@ -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"; @@ -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"; diff --git a/APIKit/APIKit.swift b/APIKit/APIKit.swift index fad842e8..f68befe3 100644 --- a/APIKit/APIKit.swift +++ b/APIKit/APIKit.swift @@ -26,16 +26,45 @@ 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) } @@ -43,6 +72,48 @@ public class API { 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? { @@ -77,11 +148,13 @@ public class API { // send request and build response object public class func sendRequest(request: T, handler: (Result) -> 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 @@ -94,18 +167,24 @@ public class API { dispatch_async(mainQueue, { handler(.Failure(Box(error))) }) return } - - let mappedResponse: Result = 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 = 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() @@ -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) + } } diff --git a/APIKitTests/APITests.swift b/APIKitTests/APITests.swift new file mode 100644 index 00000000..89c56b17 --- /dev/null +++ b/APIKitTests/APITests.swift @@ -0,0 +1,172 @@ +import Foundation +import APIKit +import XCTest +import Assertions +import OHHTTPStubs + +class APITests: XCTestCase { + class MockAPI: API { + override class func baseURL() -> NSURL { + return NSURL(string: "https://api.github.com")! + } + + override class func requestBodyBuilder() -> RequestBodyBuilder { + return .JSON(writingOptions: nil) + } + + override class func responseBodyParser() -> ResponseBodyParser { + return .JSON(readingOptions: nil) + } + + class Endpoint { + class Get: Request { + typealias Response = [String: AnyObject] + + var URLRequest: NSURLRequest? { + return MockAPI.URLRequest(.GET, "/") + } + + func responseFromObject(object: AnyObject) -> Response? { + return object as? [String: AnyObject] + } + } + } + } + + class AnotherMockAPI: API { + } + + override func tearDown() { + OHHTTPStubs.removeAllStubs() + super.tearDown() + } + + // MARK: - instance tests + func testDifferentSessionsAreCreatedForEachClasses() { + assert(MockAPI.URLSession, !=, AnotherMockAPI.URLSession) + } + + func testSameSessionsAreUsedInSameClasses() { + assertEqual(MockAPI.URLSession, MockAPI.URLSession) + assertEqual(AnotherMockAPI.URLSession, AnotherMockAPI.URLSession) + } + + func testDelegateOfSessions() { + assertNotNil(MockAPI.URLSession.delegate as? MockAPI) + assertNotNil(AnotherMockAPI.URLSession.delegate as? AnotherMockAPI) + } + + // MARK: - integration tests + func testSuccess() { + let dictionary = ["key": "value"] + let data = NSJSONSerialization.dataWithJSONObject(dictionary, options: nil, error: nil)! + + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) + }) + + let expectation = expectationWithDescription("wait for response") + let request = MockAPI.Endpoint.Get() + + MockAPI.sendRequest(request) { response in + switch response { + case .Success(let box): + assert(box.unbox, ==, dictionary) + + case .Failure: + XCTFail() + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testFailureOfConnection() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil) + + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(error: error) + }) + + let expectation = expectationWithDescription("wait for response") + let request = MockAPI.Endpoint.Get() + + MockAPI.sendRequest(request) { response in + switch response { + case .Success: + XCTFail() + + case .Failure(let box): + let error = box.unbox + assertEqual(error.domain, error.domain) + assertEqual(error.code, error.code) + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testFailureOfResponseStatusCode() { + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(data: NSData(), statusCode: 400, headers: nil) + }) + + let expectation = expectationWithDescription("wait for response") + let request = MockAPI.Endpoint.Get() + + MockAPI.sendRequest(request) { response in + switch response { + case .Success: + XCTFail() + + case .Failure(let box): + let error = box.unbox + assertEqual(error.domain, APIKitErrorDomain) + assertEqual(error.code, 400) + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } + + func testFailureOfDecodingResponseBody() { + let data = "{\"broken\": \"json}".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! + + OHHTTPStubs.stubRequestsPassingTest({ request in + return true + }, withStubResponse: { request in + return OHHTTPStubsResponse(data: data, statusCode: 200, headers: nil) + }) + + let expectation = expectationWithDescription("wait for response") + let request = MockAPI.Endpoint.Get() + + MockAPI.sendRequest(request) { response in + switch response { + case .Success: + XCTFail() + + case .Failure(let box): + let error = box.unbox + assert(error.domain, ==, NSCocoaErrorDomain) + assertEqual(error.code, 3840) + } + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(1.0, handler: nil) + } +} diff --git a/Cartfile.private b/Cartfile.private index 9d8994b1..05269001 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1 +1,2 @@ github "ikesyo/Assertions" "swift-1.1" +github "ishkawa/OHHTTPStubs" "master" diff --git a/Cartfile.resolved b/Cartfile.resolved index 7acf9a1e..2fcbaab4 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,3 @@ github "ikesyo/Assertions" "fec437ce6857259ac8bda3eb255c5782aa798734" github "LlamaKit/LlamaKit" "v0.5.0" +github "ishkawa/OHHTTPStubs" "825806534aa2bdc6ebba5e26cd304de2ced67395" diff --git a/Carthage/Checkouts/OHHTTPStubs b/Carthage/Checkouts/OHHTTPStubs new file mode 160000 index 00000000..07609ab9 --- /dev/null +++ b/Carthage/Checkouts/OHHTTPStubs @@ -0,0 +1 @@ +Subproject commit 07609ab981e12076a51bd9e2c8f06e4e73355fe2 diff --git a/README.md b/README.md index 0ca43fa2..21c385de 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,26 @@ class GitHub: API { } ``` +## Advanced usage + + +### NSURLSessionDelegate + +APIKit creates singleton instances for each subclasses of API and set them as delegates of NSURLSession, +so you can add following features by implementing delegate methods. + +- Hook events of NSURLSession +- Handle authentication challenges +- Convert a data task to NSURLSessionDownloadTask + +#### Overriding delegate methods implemented by API + +API class also uses delegate methods of NSURLSession to implement wrapper of NSURLSession, so you should call super if you override following methods. + +- `func URLSession(session:task:didCompleteWithError:)` +- `func URLSession(session:dataTask:didReceiveData:)` + + ## License Copyright (c) 2015 Yosuke Ishikawa