From f88d9c97f6ae43224cae1b3102c60e54ba985765 Mon Sep 17 00:00:00 2001 From: matsuda Date: Mon, 9 Jan 2017 02:13:17 +0900 Subject: [PATCH 01/18] Add QueryParameters protocol to provide interface for URL query. (cherry picked from commit 263ba3f6009a35f9e19a0c6594c3ca184f603b88) --- APIKit.xcodeproj/project.pbxproj | 29 +++++++++++++++++++ .../QueryParameters/QueryParameters.swift | 7 +++++ .../URLEncodedQueryParameters.swift | 20 +++++++++++++ Sources/APIKit/Request.swift | 12 ++++---- .../URLEncodedQueryParametersTests.swift | 19 ++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 Sources/APIKit/QueryParameters/QueryParameters.swift create mode 100644 Sources/APIKit/QueryParameters/URLEncodedQueryParameters.swift create mode 100644 Tests/APIKitTests/QueryParameters/URLEncodedQueryParametersTests.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index e460ab57..4b034458 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -50,6 +50,9 @@ C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; }; C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; + C5FF1DCF28A835600059573D /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCD28A835600059573D /* QueryParameters.swift */; }; + C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */; }; + C5FF1DD328A835680059573D /* URLEncodedQueryParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DD228A835680059573D /* URLEncodedQueryParametersTests.swift */; }; ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; }; ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; }; ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */; }; @@ -133,6 +136,9 @@ C5725F4A28D8C36500810D7C /* Concurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = ""; }; C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; + C5FF1DCD28A835600059573D /* QueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; }; + C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParameters.swift; sourceTree = ""; }; + C5FF1DD228A835680059573D /* URLEncodedQueryParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParametersTests.swift; sourceTree = ""; }; ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufDataParser.swift; path = Sources/APIKit/DataParser/ProtobufDataParser.swift; sourceTree = SOURCE_ROOT; }; ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtobufDataParserTests.swift; sourceTree = ""; }; ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufBodyParameters.swift; path = Sources/APIKit/BodyParameters/ProtobufBodyParameters.swift; sourceTree = SOURCE_ROOT; }; @@ -246,6 +252,7 @@ 7F698E4A1D9D680C00F1561D /* SessionTests.swift */, C5B144D628D8D7D000E30ECD /* Concurrency */, 0973EE33259E2DD000879BA2 /* Combine */, + C5FF1DD128A835680059573D /* QueryParameters */, 7F698E3B1D9D680C00F1561D /* BodyParametersType */, 7F698E401D9D680C00F1561D /* DataParserType */, 7F698E461D9D680C00F1561D /* SessionAdapterType */, @@ -318,6 +325,7 @@ C5725F4928D8C36500810D7C /* Concurrency */, 0969AE0D259DEC3C00C498AF /* Combine */, 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, + C5FF1DCC28A835600059573D /* QueryParameters */, 7F18BD0D1C972C38003A31DF /* BodyParameters */, 7FA19A441C9CC9A2005D25AE /* DataParser */, 7F18BD161C9730ED003A31DF /* Serializations */, @@ -395,6 +403,24 @@ path = Resources; sourceTree = ""; }; + C5FF1DCC28A835600059573D /* QueryParameters */ = { + isa = PBXGroup; + children = ( + C5FF1DCD28A835600059573D /* QueryParameters.swift */, + C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */, + ); + name = QueryParameters; + path = APIKit/QueryParameters; + sourceTree = ""; + }; + C5FF1DD128A835680059573D /* QueryParameters */ = { + isa = PBXGroup; + children = ( + C5FF1DD228A835680059573D /* URLEncodedQueryParametersTests.swift */, + ); + path = QueryParameters; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -515,6 +541,7 @@ 7F7048F01D9D8A12003C99F6 /* ResponseError.swift in Sources */, 7F7048EA1D9D8A08003C99F6 /* JSONDataParser.swift in Sources */, 7F7048D21D9D89BE003C99F6 /* Session.swift in Sources */, + C5FF1DCF28A835600059573D /* QueryParameters.swift in Sources */, 7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */, 7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */, 7F7048E21D9D89FB003C99F6 /* JSONBodyParameters.swift in Sources */, @@ -523,6 +550,7 @@ 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */, 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */, ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */, + C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */, 7F7048E11D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift in Sources */, 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */, ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, @@ -548,6 +576,7 @@ C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, + C5FF1DD328A835680059573D /* URLEncodedQueryParametersTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, 7F698E561D9D680C00F1561D /* StringDataParserTests.swift in Sources */, diff --git a/Sources/APIKit/QueryParameters/QueryParameters.swift b/Sources/APIKit/QueryParameters/QueryParameters.swift new file mode 100644 index 00000000..6e9f16a5 --- /dev/null +++ b/Sources/APIKit/QueryParameters/QueryParameters.swift @@ -0,0 +1,7 @@ +import Foundation + +/// `QueryParameters` provides interface to generate HTTP URL query strings. +public protocol QueryParameters { + /// Generate URL query strings. + func encode() -> String? +} diff --git a/Sources/APIKit/QueryParameters/URLEncodedQueryParameters.swift b/Sources/APIKit/QueryParameters/URLEncodedQueryParameters.swift new file mode 100644 index 00000000..e4c0f476 --- /dev/null +++ b/Sources/APIKit/QueryParameters/URLEncodedQueryParameters.swift @@ -0,0 +1,20 @@ +import Foundation + +/// `URLEncodedQueryParameters` serializes form object for HTTP URL query. +public struct URLEncodedQueryParameters: QueryParameters { + /// The parameters to be url encoded. + public let parameters: Any + + /// Returns `URLEncodedQueryParameters` that is initialized with parameters. + public init(parameters: Any) { + self.parameters = parameters + } + + /// Generate url encoded `String`. + public func encode() -> String? { + guard let parameters = parameters as? [String: Any], !parameters.isEmpty else { + return nil + } + return URLEncodedSerialization.string(from: parameters) + } +} diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request.swift index ea6c5dae..8e7c02ae 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request.swift @@ -28,7 +28,7 @@ public protocol Request { /// The actual parameters for the URL query. The values of this property will be escaped using `URLEncodedSerialization`. /// If this property is not implemented and `method.prefersQueryParameter` is `true`, the value of this property /// will be computed from `parameters`. - var queryParameters: [String: Any]? { get } + var queryParameters: QueryParameters? { get } /// The actual parameters for the HTTP body. If this property is not implemented and `method.prefersQueryParameter` is `false`, /// the value of this property will be computed from `parameters` using `JSONBodyParameters`. @@ -65,12 +65,12 @@ public extension Request { return nil } - var queryParameters: [String: Any]? { - guard let parameters = parameters as? [String: Any], method.prefersQueryParameters else { + var queryParameters: QueryParameters? { + guard let parameters = parameters, method.prefersQueryParameters else { return nil } - return parameters + return URLEncodedQueryParameters(parameters: parameters) } var bodyParameters: BodyParameters? { @@ -110,8 +110,8 @@ public extension Request { var urlRequest = URLRequest(url: url) - if let queryParameters = queryParameters, !queryParameters.isEmpty { - components.percentEncodedQuery = URLEncodedSerialization.string(from: queryParameters) + if let queryString = queryParameters?.encode(), !queryString.isEmpty { + components.percentEncodedQuery = queryString } if let bodyParameters = bodyParameters { diff --git a/Tests/APIKitTests/QueryParameters/URLEncodedQueryParametersTests.swift b/Tests/APIKitTests/QueryParameters/URLEncodedQueryParametersTests.swift new file mode 100644 index 00000000..a764e00c --- /dev/null +++ b/Tests/APIKitTests/QueryParameters/URLEncodedQueryParametersTests.swift @@ -0,0 +1,19 @@ +import XCTest +import APIKit + +class URLEncodedQueryParametersTests: XCTestCase { + func testURLEncodedSuccess() { + let object: [String: Any] = ["foo": "string", "bar": 1, "q": "こんにちは"] + let parameters = URLEncodedQueryParameters(parameters: object) + guard let query = parameters.encode() else { + XCTFail() + return + } + + let items = query.components(separatedBy: "&") + XCTAssertEqual(items.count, 3) + XCTAssertTrue(items.contains("foo=string")) + XCTAssertTrue(items.contains("bar=1")) + XCTAssertTrue(items.contains("q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF")) + } +} From 10eff1594d661919457eea822f508093da4443c0 Mon Sep 17 00:00:00 2001 From: taish Date: Wed, 22 Mar 2017 22:18:34 +0900 Subject: [PATCH 02/18] Add a way to get progress of uploading --- Sources/APIKit/Concurrency/Concurrency.swift | 2 +- Sources/APIKit/Session.swift | 51 ++++++++++--------- .../SessionAdapter/SessionAdapter.swift | 2 +- .../SessionAdapter/URLSessionAdapter.swift | 16 +++++- .../URLSessionAdapterSubclassTests.swift | 26 ++++++++++ Tests/APIKitTests/SessionTests.swift | 24 +++++++-- .../TestComponents/TestSessionAdapter.swift | 5 +- .../TestComponents/TestSessionTask.swift | 6 ++- 8 files changed, 99 insertions(+), 33 deletions(-) diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index c0518e35..a2442256 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -23,7 +23,7 @@ public extension Session { return try await withTaskCancellationHandler(operation: { return try await withCheckedThrowingContinuation { continuation in Task { - let sessionTask = createSessionTask(request, callbackQueue: callbackQueue) { result in + let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _, _, _ in }) { result in continuation.resume(with: result) } await cancellationHandler.register(with: sessionTask) diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 85fb9615..6bc3fed9 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -36,8 +36,8 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - return shared.send(request, callbackQueue: callbackQueue, handler: handler) + open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, handler: handler) } /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. @@ -54,8 +54,8 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - let task = createSessionTask(request, callbackQueue: callbackQueue, handler: handler) + open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, handler: handler) task?.resume() return task } @@ -77,7 +77,7 @@ open class Session { } } - internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Result) -> Void) -> SessionTask? { let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { @@ -89,28 +89,33 @@ open class Session { return nil } - let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in - let result: Result - - switch (data, urlResponse, error) { - case (_, _, let error?): - result = .failure(.connectionError(error)) + let task = adapter.createTask(with: urlRequest, + progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in + progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend) + }, + handler: { data, urlResponse, error in + let result: Result + + switch (data, urlResponse, error) { + case (_, _, let error?): + result = .failure(.connectionError(error)) + + case (let data?, let urlResponse as HTTPURLResponse, _): + do { + result = .success(try request.parse(data: data as Data, urlResponse: urlResponse)) + } catch { + result = .failure(.responseError(error)) + } - case (let data?, let urlResponse as HTTPURLResponse, _): - do { - result = .success(try request.parse(data: data as Data, urlResponse: urlResponse)) - } catch { - result = .failure(.responseError(error)) + default: + result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse))) } - default: - result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse))) - } - - callbackQueue.execute { - handler(result) + callbackQueue.execute { + handler(result) + } } - } + ) setRequest(request, forTask: task) diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index aa71ac5d..034fd7c9 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -11,7 +11,7 @@ public protocol SessionTask: AnyObject { /// with `Session`. public protocol SessionAdapter { /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. - func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask /// Collects tasks from backend networking stack. `handler` must be called after collecting. func getTasks(with handler: @escaping ([SessionTask]) -> Void) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index 38ac15ef..cc89312c 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -6,6 +6,7 @@ extension URLSessionTask: SessionTask { private var dataTaskResponseBufferKey = 0 private var taskAssociatedObjectCompletionHandlerKey = 0 +private var taskAssociatedObjectProgressHandlerKey = 0 /// `URLSessionAdapter` connects `URLSession` with `Session`. /// @@ -25,11 +26,12 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. - open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { + open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { let task = urlSession.dataTask(with: URLRequest) setBuffer(NSMutableData(), forTask: task) setHandler(handler, forTask: task) + setProgressHandler(progressHandler, forTask: task) return task } @@ -58,6 +60,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void } + private func setProgressHandler(_ progressHandler: @escaping (Int64, Int64, Int64) -> Void, forTask task: URLSessionTask) { + objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? { + return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Int64, Int64, Int64) -> Void + } // MARK: URLSessionTaskDelegate open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { handler(for: task)?(buffer(for: task) as Data?, task.response, error) @@ -67,4 +76,9 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { buffer(for: dataTask)?.append(data) } + + // MARK: URLSessionDataDelegate + open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + progressHandler(for: task)?(bytesSent, totalBytesSent, totalBytesExpectedToSend) + } } diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 4820dde3..8836c458 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -15,6 +15,11 @@ class URLSessionAdapterSubclassTests: XCTestCase { functionCallFlags[(#function)] = true super.urlSession(session, dataTask: dataTask, didReceive: data) } + + override func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + functionCallFlags[(#function)] = true + super.urlSession(session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend) + } } var adapter: SessionAdapter! @@ -50,4 +55,25 @@ class URLSessionAdapterSubclassTests: XCTestCase { XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didCompleteWithError:)"], true) XCTAssertEqual(adapter.functionCallFlags["urlSession(_:dataTask:didReceive:)"], true) } + + // Limitation: 'urlSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:' delegate method will never be called when you stub the request using subclass of URLProtocol. + func testDelegateProgressMethodCall() { + let expectation = self.expectation(description: "wait for response") + let request = TestRequest(baseURL: "https://httpbin.org", path: "/post", method: .post) + let configuration = URLSessionConfiguration.default + let adapter = SessionAdapter(configuration: configuration) + let session = Session(adapter: adapter) + + session.send(request, + handler: { result in + if case .failure = result { + XCTFail() + } + + expectation.fulfill() + }) + + waitForExpectations(timeout: 10.0, handler: nil) + XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)"], true) + } } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 40b887fc..2948b13a 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -218,6 +218,23 @@ class SessionTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } + func testProgress() { + let dictionary = ["key": "value"] + adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest(method: .post) + + session.send(request, progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in + XCTAssertNotNil(bytesSent) + XCTAssertNotNil(totalBytesSent) + XCTAssertNotNil(totalBytesExpectedToSend) + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + // MARK: Class methods func testSharedSession() { XCTAssert(Session.shared === Session.shared) @@ -233,12 +250,13 @@ class SessionTests: XCTestCase { return testSesssion } - override func send(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { + override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Result) -> Void) -> SessionTask? { + functionCallFlags[(#function)] = true return super.send(request) } - override func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) { + override func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) { functionCallFlags[(#function)] = true } } @@ -247,7 +265,7 @@ class SessionTests: XCTestCase { SessionSubclass.send(TestRequest()) SessionSubclass.cancelRequests(with: TestRequest.self) - XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:handler:)"], true) + XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:handler:)"], true) XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true) } } diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index 4813c23c..8f879b4e 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -42,6 +42,7 @@ class TestSessionAdapter: SessionAdapter { if task.cancelled { task.handler(nil, nil, Error.cancelled) } else { + task.progressHandler(1, 1, 1) task.handler(data, urlResponse, error) } } @@ -49,8 +50,8 @@ class TestSessionAdapter: SessionAdapter { tasks = [] } - func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { - let task = TestSessionTask(handler: handler) + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { + let task = TestSessionTask(progressHandler: progressHandler, handler: handler) tasks.append(task) return task diff --git a/Tests/APIKitTests/TestComponents/TestSessionTask.swift b/Tests/APIKitTests/TestComponents/TestSessionTask.swift index 5bf7927a..745cf24f 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionTask.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionTask.swift @@ -2,12 +2,14 @@ import Foundation import APIKit class TestSessionTask: SessionTask { - + var handler: (Data?, URLResponse?, Error?) -> Void + var progressHandler: (Int64, Int64, Int64) -> Void var cancelled = false - init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) { + init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) { self.handler = handler + self.progressHandler = progressHandler } func resume() { From 86174848ad2a246d1672ed10382132b8f9804c22 Mon Sep 17 00:00:00 2001 From: taish Date: Sat, 25 Mar 2017 16:41:22 +0900 Subject: [PATCH 03/18] rename argment `progressHandler` to `completionHandler` in `Session` class. --- Sources/APIKit/Session.swift | 14 +++++++------- .../URLSessionAdapterSubclassTests.swift | 2 +- Tests/APIKitTests/SessionTests.swift | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 6bc3fed9..fe0e34e4 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -36,8 +36,8 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, handler: handler) + open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) } /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. @@ -54,8 +54,8 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, handler: handler) + open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) task?.resume() return task } @@ -77,14 +77,14 @@ open class Session { } } - internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Result) -> Void) -> SessionTask? { + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { urlRequest = try request.buildURLRequest() } catch { callbackQueue.execute { - handler(.failure(.requestError(error))) + completionHandler(.failure(.requestError(error))) } return nil } @@ -112,7 +112,7 @@ open class Session { } callbackQueue.execute { - handler(result) + completionHandler(result) } } ) diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 8836c458..6e03bf67 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -65,7 +65,7 @@ class URLSessionAdapterSubclassTests: XCTestCase { let session = Session(adapter: adapter) session.send(request, - handler: { result in + completionHandler: { result in if case .failure = result { XCTFail() } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 2948b13a..7c28e038 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -250,7 +250,7 @@ class SessionTests: XCTestCase { return testSesssion } - override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Result) -> Void) -> SessionTask? { + override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { functionCallFlags[(#function)] = true return super.send(request) @@ -265,7 +265,7 @@ class SessionTests: XCTestCase { SessionSubclass.send(TestRequest()) SessionSubclass.cancelRequests(with: TestRequest.self) - XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:handler:)"], true) + XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true) XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true) } } From c74cce79af263b7ff741c6cedcba44e2d613661c Mon Sep 17 00:00:00 2001 From: taish Date: Sat, 25 Mar 2017 17:20:20 +0900 Subject: [PATCH 04/18] rename argment `progressHandler` to `completionHandler` in `SessionAdapter` protocol. (cherry picked from commit aa088180ffb2d28b43afbc060a7f98c520a34594) --- Sources/APIKit/Session.swift | 2 +- Sources/APIKit/SessionAdapter/SessionAdapter.swift | 2 +- Sources/APIKit/SessionAdapter/URLSessionAdapter.swift | 4 ++-- Tests/APIKitTests/TestComponents/TestSessionAdapter.swift | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index fe0e34e4..6eb2380a 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -93,7 +93,7 @@ open class Session { progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend) }, - handler: { data, urlResponse, error in + completionHandler: { data, urlResponse, error in let result: Result switch (data, urlResponse, error) { diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index 034fd7c9..5a8eba3b 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -11,7 +11,7 @@ public protocol SessionTask: AnyObject { /// with `Session`. public protocol SessionAdapter { /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask /// Collects tasks from backend networking stack. `handler` must be called after collecting. func getTasks(with handler: @escaping ([SessionTask]) -> Void) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index cc89312c..a5818a3b 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -26,11 +26,11 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. - open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { + open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { let task = urlSession.dataTask(with: URLRequest) setBuffer(NSMutableData(), forTask: task) - setHandler(handler, forTask: task) + setHandler(completionHandler, forTask: task) setProgressHandler(progressHandler, forTask: task) return task diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index 8f879b4e..c4fb86ae 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -50,8 +50,8 @@ class TestSessionAdapter: SessionAdapter { tasks = [] } - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { - let task = TestSessionTask(progressHandler: progressHandler, handler: handler) + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { + let task = TestSessionTask(progressHandler: progressHandler, handler: completionHandler) tasks.append(task) return task From e83b529f1a9a6a632ac69ae0324efa43b439c966 Mon Sep 17 00:00:00 2001 From: taish Date: Sat, 25 Mar 2017 17:24:34 +0900 Subject: [PATCH 05/18] rename argment `progressHandler` to `completionHandler` in `TestSessionTask` (cherry picked from commit 87488209692fb1e90b074b83f50943733ccf8254) --- Tests/APIKitTests/TestComponents/TestSessionAdapter.swift | 6 +++--- Tests/APIKitTests/TestComponents/TestSessionTask.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index c4fb86ae..b9c3cdb1 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -40,10 +40,10 @@ class TestSessionAdapter: SessionAdapter { func executeAllTasks() { for task in tasks { if task.cancelled { - task.handler(nil, nil, Error.cancelled) + task.completionHandler(nil, nil, Error.cancelled) } else { task.progressHandler(1, 1, 1) - task.handler(data, urlResponse, error) + task.completionHandler(data, urlResponse, error) } } @@ -51,7 +51,7 @@ class TestSessionAdapter: SessionAdapter { } func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { - let task = TestSessionTask(progressHandler: progressHandler, handler: completionHandler) + let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler) tasks.append(task) return task diff --git a/Tests/APIKitTests/TestComponents/TestSessionTask.swift b/Tests/APIKitTests/TestComponents/TestSessionTask.swift index 745cf24f..af230b48 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionTask.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionTask.swift @@ -3,12 +3,12 @@ import APIKit class TestSessionTask: SessionTask { - var handler: (Data?, URLResponse?, Error?) -> Void + var completionHandler: (Data?, URLResponse?, Error?) -> Void var progressHandler: (Int64, Int64, Int64) -> Void var cancelled = false - init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, handler: @escaping (Data?, URLResponse?, Error?) -> Void) { - self.handler = handler + init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { + self.completionHandler = completionHandler self.progressHandler = progressHandler } From c0d5c4c8a69aa694b13df74ece55cfaf4ae52662 Mon Sep 17 00:00:00 2001 From: taish Date: Wed, 29 Mar 2017 16:30:21 +0900 Subject: [PATCH 06/18] Fixed `progressHandler` to work properly (cherry picked from commit 20eebe810489a3c3dbb100c72701e69bf0c5f0d0) --- Sources/APIKit/SessionAdapter/URLSessionAdapter.swift | 2 +- .../URLSessionAdapterSubclassTests.swift | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index a5818a3b..3077dc8c 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -65,7 +65,7 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? { - return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Int64, Int64, Int64) -> Void + return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Int64, Int64, Int64) -> Void } // MARK: URLSessionTaskDelegate open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 6e03bf67..a75b329e 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -64,14 +64,9 @@ class URLSessionAdapterSubclassTests: XCTestCase { let adapter = SessionAdapter(configuration: configuration) let session = Session(adapter: adapter) - session.send(request, - completionHandler: { result in - if case .failure = result { - XCTFail() - } - - expectation.fulfill() - }) + session.send(request, progressHandler: { _, _, _ in + expectation.fulfill() + }) waitForExpectations(timeout: 10.0, handler: nil) XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)"], true) From f1e774dfa97a5b9ffcbcf0177db1f4c2cf38fc79 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 04:59:23 +0900 Subject: [PATCH 07/18] Fix Xcode build errors and warnings --- Sources/APIKit/Combine/Combine.swift | 4 +- .../URLSessionAdapterSubclassTests.swift | 4 +- .../URLSessionAdapterTests.swift | 16 +++---- .../SessionCallbackQueueTests.swift | 24 +++++----- Tests/APIKitTests/SessionTests.swift | 44 +++++++++---------- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift index 6e3223c0..2c525e5c 100644 --- a/Sources/APIKit/Combine/Combine.swift +++ b/Sources/APIKit/Combine/Combine.swift @@ -47,7 +47,7 @@ public struct SessionTaskPublisher: Publisher { assert(demand > 0) guard let downstream = self.downstream else { return } self.downstream = nil - task = session.send(request, callbackQueue: callbackQueue) { result in + task = session.send(request, callbackQueue: callbackQueue, completionHandler: { result in switch result { case .success(let response): _ = downstream.receive(response) @@ -55,7 +55,7 @@ public struct SessionTaskPublisher: Publisher { case .failure(let error): downstream.receive(completion: .failure(error)) } - } + }) } func cancel() { diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index a75b329e..84df8e84 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -42,13 +42,13 @@ class URLSessionAdapterSubclassTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure = result { XCTFail() } expectation.fulfill() - } + }) waitForExpectations(timeout: 10.0, handler: nil) diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterTests.swift index 8b9be0be..6e78afdd 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterTests.swift @@ -24,7 +24,7 @@ class URLSessionAdapterTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { response in + session.send(request, completionHandler: { response in switch response { case .success(let dictionary): XCTAssertEqual((dictionary as? [String: String])?["key"], "value") @@ -32,9 +32,9 @@ class URLSessionAdapterTests: XCTestCase { case .failure: XCTFail() } - + expectation.fulfill() - } + }) waitForExpectations(timeout: 10.0, handler: nil) } @@ -46,11 +46,11 @@ class URLSessionAdapterTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { response in + session.send(request, completionHandler: { response in switch response { case .success: XCTFail() - + case .failure(let error): switch error { case .connectionError(let error as NSError): @@ -62,7 +62,7 @@ class URLSessionAdapterTests: XCTestCase { } expectation.fulfill() - } + }) waitForExpectations(timeout: 10.0, handler: nil) } @@ -74,7 +74,7 @@ class URLSessionAdapterTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in guard case .failure(let error) = result, case .connectionError(let connectionError as NSError) = error else { XCTFail() @@ -84,7 +84,7 @@ class URLSessionAdapterTests: XCTestCase { XCTAssertEqual(connectionError.code, NSURLErrorCancelled) expectation.fulfill() - } + }) DispatchQueue.main.async { self.session.cancelRequests(with: TestRequest.self) diff --git a/Tests/APIKitTests/SessionCallbackQueueTests.swift b/Tests/APIKitTests/SessionCallbackQueueTests.swift index b9d961f9..97db037e 100644 --- a/Tests/APIKitTests/SessionCallbackQueueTests.swift +++ b/Tests/APIKitTests/SessionCallbackQueueTests.swift @@ -19,10 +19,10 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request, callbackQueue: .main) { result in + session.send(request, callbackQueue: .main, completionHandler: { result in XCTAssert(Thread.isMainThread) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -31,11 +31,11 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request, callbackQueue: .sessionQueue) { result in + session.send(request, callbackQueue: .sessionQueue, completionHandler: { result in // This depends on implementation of TestSessionAdapter XCTAssertTrue(Thread.isMainThread) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -45,10 +45,10 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request, callbackQueue: .operationQueue(operationQueue)) { result in + session.send(request, callbackQueue: .operationQueue(operationQueue), completionHandler: { result in XCTAssertEqual(OperationQueue.current, operationQueue) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -58,11 +58,11 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request, callbackQueue: .dispatchQueue(dispatchQueue)) { result in + session.send(request, callbackQueue: .dispatchQueue(dispatchQueue), completionHandler: { result in // There is no way to test current dispatch queue. XCTAssertFalse(Thread.isMainThread) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -75,10 +75,10 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in XCTAssertEqual(OperationQueue.current, operationQueue) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -90,10 +90,10 @@ class SessionCallbackQueueTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request, callbackQueue: nil) { result in + session.send(request, callbackQueue: nil, completionHandler: { result in XCTAssertEqual(OperationQueue.current, operationQueue) expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 7c28e038..d7ac2607 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -20,7 +20,7 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { response in + session.send(request, completionHandler: { response in switch response { case .success(let dictionary): XCTAssertEqual((dictionary as? [String: String])?["key"], "value") @@ -28,9 +28,9 @@ class SessionTests: XCTestCase { case .failure: XCTFail() } - + expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -42,7 +42,7 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure(let error) = result, case .responseError(let responseError as NSError) = error { XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) @@ -50,9 +50,9 @@ class SessionTests: XCTestCase { } else { XCTFail() } - + expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -63,7 +63,7 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure(let error) = result, case .responseError(let responseError as ResponseError) = error, case .unacceptableStatusCode(let statusCode) = responseError { @@ -73,7 +73,7 @@ class SessionTests: XCTestCase { } expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -84,7 +84,7 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure(let error) = result, case .responseError(let responseError as ResponseError) = error, case .nonHTTPURLResponse(let urlResponse) = responseError { @@ -94,7 +94,7 @@ class SessionTests: XCTestCase { } expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) } @@ -108,7 +108,7 @@ class SessionTests: XCTestCase { throw Error() } - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure(let error) = result, case .requestError(let requestError) = error { XCTAssert(requestError is Error) @@ -117,7 +117,7 @@ class SessionTests: XCTestCase { } expectation.fulfill() - } + }) waitForExpectations(timeout: 1.0, handler: nil) @@ -128,7 +128,7 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.send(request) { result in + session.send(request, completionHandler: { result in if case .failure(let error) = result, case .connectionError(let connectionError as NSError) = error { XCTAssertEqual(connectionError.code, 0) @@ -137,7 +137,7 @@ class SessionTests: XCTestCase { } expectation.fulfill() - } + }) session.cancelRequests(with: TestRequest.self) @@ -148,24 +148,24 @@ class SessionTests: XCTestCase { let successExpectation = expectation(description: "wait for response") let successRequest = TestRequest(path: "/success") - session.send(successRequest) { result in + session.send(successRequest, completionHandler: { result in if case .failure = result { XCTFail() } successExpectation.fulfill() - } + }) let failureExpectation = expectation(description: "wait for response") let failureRequest = TestRequest(path: "/failure") - session.send(failureRequest) { result in + session.send(failureRequest, completionHandler: { result in if case .success = result { XCTFail() } failureExpectation.fulfill() - } + }) session.cancelRequests(with: TestRequest.self) { request in return request.path == failureRequest.path @@ -194,24 +194,24 @@ class SessionTests: XCTestCase { let successExpectation = expectation(description: "wait for response") let successRequest = AnotherTestRequest() - session.send(successRequest) { result in + session.send(successRequest, completionHandler: { result in if case .failure = result { XCTFail() } successExpectation.fulfill() - } + }) let failureExpectation = expectation(description: "wait for response") let failureRequest = TestRequest() - session.send(failureRequest) { result in + session.send(failureRequest, completionHandler: { result in if case .success = result { XCTFail() } failureExpectation.fulfill() - } + }) session.cancelRequests(with: TestRequest.self) From ee5fa5174470c6b27b15c00539a0c2dc72312459 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 05:18:16 +0900 Subject: [PATCH 08/18] Change to use `Foundation.Progress` --- Sources/APIKit/Concurrency/Concurrency.swift | 2 +- Sources/APIKit/Session.swift | 10 +++++----- Sources/APIKit/SessionAdapter/SessionAdapter.swift | 2 +- .../APIKit/SessionAdapter/URLSessionAdapter.swift | 13 ++++++++----- .../URLSessionAdapterSubclassTests.swift | 2 +- Tests/APIKitTests/SessionTests.swift | 8 +++----- .../TestComponents/TestSessionAdapter.swift | 4 ++-- .../TestComponents/TestSessionTask.swift | 4 ++-- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index a2442256..c2a0e7a3 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -23,7 +23,7 @@ public extension Session { return try await withTaskCancellationHandler(operation: { return try await withCheckedThrowingContinuation { continuation in Task { - let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _, _, _ in }) { result in + let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _ in }) { result in continuation.resume(with: result) } await cancellationHandler.register(with: sessionTask) diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 6eb2380a..9a63e179 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -36,7 +36,7 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) } @@ -54,7 +54,7 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _, _, _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) task?.resume() return task @@ -77,7 +77,7 @@ open class Session { } } - internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { @@ -90,8 +90,8 @@ open class Session { } let task = adapter.createTask(with: urlRequest, - progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in - progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend) + progressHandler: { progress in + progressHandler(progress) }, completionHandler: { data, urlResponse, error in let result: Result diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index 5a8eba3b..bf6f164d 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -11,7 +11,7 @@ public protocol SessionTask: AnyObject { /// with `Session`. public protocol SessionAdapter { /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask /// Collects tasks from backend networking stack. `handler` must be called after collecting. func getTasks(with handler: @escaping ([SessionTask]) -> Void) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index 3077dc8c..52f7f902 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -26,7 +26,7 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. - open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { + open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { let task = urlSession.dataTask(with: URLRequest) setBuffer(NSMutableData(), forTask: task) @@ -60,13 +60,14 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void } - private func setProgressHandler(_ progressHandler: @escaping (Int64, Int64, Int64) -> Void, forTask task: URLSessionTask) { + private func setProgressHandler(_ progressHandler: @escaping (Progress) -> Void, forTask task: URLSessionTask) { objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? { - return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Int64, Int64, Int64) -> Void + private func progressHandler(for task: URLSessionTask) -> ((Progress) -> Void)? { + return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Progress) -> Void } + // MARK: URLSessionTaskDelegate open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { handler(for: task)?(buffer(for: task) as Data?, task.response, error) @@ -79,6 +80,8 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS // MARK: URLSessionDataDelegate open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - progressHandler(for: task)?(bytesSent, totalBytesSent, totalBytesExpectedToSend) + let progress = Progress(totalUnitCount: totalBytesExpectedToSend) + progress.completedUnitCount = totalBytesSent + progressHandler(for: task)?(progress) } } diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 84df8e84..2afa3eac 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -64,7 +64,7 @@ class URLSessionAdapterSubclassTests: XCTestCase { let adapter = SessionAdapter(configuration: configuration) let session = Session(adapter: adapter) - session.send(request, progressHandler: { _, _, _ in + session.send(request, progressHandler: { _ in expectation.fulfill() }) diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index d7ac2607..5e93a163 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -225,10 +225,8 @@ class SessionTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest(method: .post) - session.send(request, progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in - XCTAssertNotNil(bytesSent) - XCTAssertNotNil(totalBytesSent) - XCTAssertNotNil(totalBytesExpectedToSend) + session.send(request, progressHandler: { progress in + XCTAssertNotNil(progress) expectation.fulfill() }) @@ -250,7 +248,7 @@ class SessionTests: XCTestCase { return testSesssion } - override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { + override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { functionCallFlags[(#function)] = true return super.send(request) diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index b9c3cdb1..99f0ebf8 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -42,7 +42,7 @@ class TestSessionAdapter: SessionAdapter { if task.cancelled { task.completionHandler(nil, nil, Error.cancelled) } else { - task.progressHandler(1, 1, 1) + task.progressHandler(Progress(totalUnitCount: 1)) task.completionHandler(data, urlResponse, error) } } @@ -50,7 +50,7 @@ class TestSessionAdapter: SessionAdapter { tasks = [] } - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler) tasks.append(task) diff --git a/Tests/APIKitTests/TestComponents/TestSessionTask.swift b/Tests/APIKitTests/TestComponents/TestSessionTask.swift index af230b48..05e3e92a 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionTask.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionTask.swift @@ -4,10 +4,10 @@ import APIKit class TestSessionTask: SessionTask { var completionHandler: (Data?, URLResponse?, Error?) -> Void - var progressHandler: (Int64, Int64, Int64) -> Void + var progressHandler: (Progress) -> Void var cancelled = false - init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { + init(progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { self.completionHandler = completionHandler self.progressHandler = progressHandler } From 7aa125d1a7b94a072191d39aefaa7532ddd7422e Mon Sep 17 00:00:00 2001 From: Econa77 Date: Mon, 17 Oct 2022 01:04:59 +0900 Subject: [PATCH 09/18] Extend timeouts for tests involving actual network request --- .../SessionAdapterType/URLSessionAdapterSubclassTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 2afa3eac..938a72d9 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -68,7 +68,7 @@ class URLSessionAdapterSubclassTests: XCTestCase { expectation.fulfill() }) - waitForExpectations(timeout: 10.0, handler: nil) + waitForExpectations(timeout: 30.0, handler: nil) XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)"], true) } } From 5fac79c8d180b055c715ed7de30c2d41374a78e7 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 15 Oct 2022 18:30:36 +0900 Subject: [PATCH 10/18] Add a way to get progress of downloading. --- Demo.playground/Contents.swift | 9 ++-- Sources/APIKit/Concurrency/Concurrency.swift | 2 +- Sources/APIKit/Session.swift | 32 ++++++++++---- .../SessionAdapter/SessionAdapter.swift | 2 +- .../SessionAdapter/URLSessionAdapter.swift | 43 +++++++++++++------ .../URLSessionAdapterSubclassTests.swift | 2 +- Tests/APIKitTests/SessionTests.swift | 23 ++++++++-- .../TestComponents/TestSessionAdapter.swift | 7 +-- .../TestComponents/TestSessionTask.swift | 9 ++-- 9 files changed, 91 insertions(+), 38 deletions(-) diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index aea5177c..18a73aca 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -60,13 +60,16 @@ struct GetRateLimitRequest: GitHubRequest { //: Step 4: Send request let request = GetRateLimitRequest() -Session.send(request) { result in +Session.send(request, uploadProgressHandler: { progress in + print("upload progress: \(progress.fractionCompleted)") +}, downloadProgressHandler: { progress in + print("download progress: \(progress.fractionCompleted) %") +}, completionHandler: { result in switch result { case .success(let rateLimit): print("count: \(rateLimit.count)") print("reset: \(rateLimit.resetDate)") - case .failure(let error): print("error: \(error)") } -} +}) diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index c2a0e7a3..82a6d458 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -23,7 +23,7 @@ public extension Session { return try await withTaskCancellationHandler(operation: { return try await withCheckedThrowingContinuation { continuation in Task { - let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _ in }) { result in + let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: { _ in }, downloadProgressHandler: { _ in }) { result in continuation.resume(with: result) } await cancellationHandler.register(with: sessionTask) diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 9a63e179..0f202db1 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -10,6 +10,9 @@ open class Session { /// The default callback queue for `send(_:handler:)`. public let callbackQueue: CallbackQueue + /// Closure type executed when the upload or download progress of a request. + public typealias ProgressHandler = (Progress) -> Void + /// Returns `Session` instance that is initialized with `adapter`. /// - parameter adapter: The adapter that connects lower level backend with Session interface. /// - parameter callbackQueue: The default callback queue for `send(_:handler:)`. @@ -33,11 +36,13 @@ open class Session { /// Calls `send(_:callbackQueue:handler:)` of `Session.shared`. /// - parameter request: The request to be sent. /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. - /// - parameter handler: The closure that receives result of the request. + /// - parameter uploadProgressHandler: The closure that receives upload progress of the request. + /// - parameter downloadProgressHandler: The closure that receives download progress of the request. + /// - parameter completionHandler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) + open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, uploadProgressHandler: @escaping ProgressHandler = { _ in }, downloadProgressHandler: @escaping ProgressHandler = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + return shared.send(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler) } /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. @@ -51,11 +56,13 @@ open class Session { /// `Request.Response` is inferred from `Request` type parameter, the it changes depending on the request type. /// - parameter request: The request to be sent. /// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used. - /// - parameter handler: The closure that receives result of the request. + /// - parameter uploadProgressHandler: The closure that receives upload progress of the request. + /// - parameter downloadProgressHandler: The closure that receives download progress of the request. + /// - parameter completionHandler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) + open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, uploadProgressHandler: @escaping ProgressHandler = { _ in }, downloadProgressHandler: @escaping ProgressHandler = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + let task = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler) task?.resume() return task } @@ -77,7 +84,7 @@ open class Session { } } - internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, uploadProgressHandler: @escaping ProgressHandler, downloadProgressHandler: @escaping ProgressHandler, completionHandler: @escaping (Result) -> Void) -> SessionTask? { let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { @@ -90,8 +97,15 @@ open class Session { } let task = adapter.createTask(with: urlRequest, - progressHandler: { progress in - progressHandler(progress) + uploadProgressHandler: { progress in + callbackQueue.execute { + uploadProgressHandler(progress) + } + }, + downloadProgressHandler: { progress in + callbackQueue.execute { + downloadProgressHandler(progress) + } }, completionHandler: { data, urlResponse, error in let result: Result diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index bf6f164d..a487108f 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -11,7 +11,7 @@ public protocol SessionTask: AnyObject { /// with `Session`. public protocol SessionAdapter { /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask + func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask /// Collects tasks from backend networking stack. `handler` must be called after collecting. func getTasks(with handler: @escaping ([SessionTask]) -> Void) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index 52f7f902..93cb58e7 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -6,7 +6,8 @@ extension URLSessionTask: SessionTask { private var dataTaskResponseBufferKey = 0 private var taskAssociatedObjectCompletionHandlerKey = 0 -private var taskAssociatedObjectProgressHandlerKey = 0 +private var taskAssociatedObjectUploadProgressHandlerKey = 0 +private var taskAssociatedObjectDownloadProgressHandlerKey = 0 /// `URLSessionAdapter` connects `URLSession` with `Session`. /// @@ -26,12 +27,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. - open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { + open func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { let task = urlSession.dataTask(with: URLRequest) setBuffer(NSMutableData(), forTask: task) setHandler(completionHandler, forTask: task) - setProgressHandler(progressHandler, forTask: task) + setUploadProgressHandler(uploadProgressHandler, forTask: task) + setDownloadProgressHandler(downloadProgressHandler, forTask: task) return task } @@ -60,12 +62,20 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void } - private func setProgressHandler(_ progressHandler: @escaping (Progress) -> Void, forTask task: URLSessionTask) { - objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + private func setUploadProgressHandler(_ progressHandler: @escaping Session.ProgressHandler, forTask task: URLSessionTask) { + objc_setAssociatedObject(task, &taskAssociatedObjectUploadProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - private func progressHandler(for task: URLSessionTask) -> ((Progress) -> Void)? { - return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Progress) -> Void + private func uploadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? { + return objc_getAssociatedObject(task, &taskAssociatedObjectUploadProgressHandlerKey) as? Session.ProgressHandler + } + + private func setDownloadProgressHandler(_ progressHandler: @escaping Session.ProgressHandler, forTask task: URLSessionTask) { + objc_setAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func downloadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? { + return objc_getAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey) as? Session.ProgressHandler } // MARK: URLSessionTaskDelegate @@ -73,15 +83,24 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS handler(for: task)?(buffer(for: task) as Data?, task.response, error) } + open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + let progress = Progress(totalUnitCount: totalBytesExpectedToSend) + progress.completedUnitCount = totalBytesSent + uploadProgressHandler(for: task)?(progress) + } + // MARK: URLSessionDataDelegate open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { buffer(for: dataTask)?.append(data) + updateDownloadProgress(dataTask) } - // MARK: URLSessionDataDelegate - open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - let progress = Progress(totalUnitCount: totalBytesExpectedToSend) - progress.completedUnitCount = totalBytesSent - progressHandler(for: task)?(progress) + private func updateDownloadProgress(_ task: URLSessionTask) { + let receivedData = buffer(for: task) as Data? + let totalBytesReceived = Int64(receivedData?.count ?? 0) + let totalBytesExpected = task.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown + let progress = Progress(totalUnitCount: totalBytesExpected) + progress.completedUnitCount = totalBytesReceived + downloadProgressHandler(for: task)?(progress) } } diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 938a72d9..dd9decc0 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -64,7 +64,7 @@ class URLSessionAdapterSubclassTests: XCTestCase { let adapter = SessionAdapter(configuration: configuration) let session = Session(adapter: adapter) - session.send(request, progressHandler: { _ in + session.send(request, uploadProgressHandler: { _ in expectation.fulfill() }) diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 5e93a163..e2fd734e 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -218,14 +218,29 @@ class SessionTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } - func testProgress() { + func testUploadProgress() { let dictionary = ["key": "value"] adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) let expectation = self.expectation(description: "wait for response") let request = TestRequest(method: .post) - session.send(request, progressHandler: { progress in + session.send(request, uploadProgressHandler: { progress in + XCTAssertNotNil(progress) + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testDownloadProgress() { + let dictionary = ["key": "value"] + adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest(method: .post) + + session.send(request, downloadProgressHandler: { progress in XCTAssertNotNil(progress) expectation.fulfill() }) @@ -248,7 +263,7 @@ class SessionTests: XCTestCase { return testSesssion } - override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { + override func send(_ request: Request, callbackQueue: CallbackQueue?, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Result) -> Void) -> SessionTask? { functionCallFlags[(#function)] = true return super.send(request) @@ -263,7 +278,7 @@ class SessionTests: XCTestCase { SessionSubclass.send(TestRequest()) SessionSubclass.cancelRequests(with: TestRequest.self) - XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true) + XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:uploadProgressHandler:downloadProgressHandler:completionHandler:)"], true) XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true) } } diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index 99f0ebf8..bddeba0c 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -42,7 +42,8 @@ class TestSessionAdapter: SessionAdapter { if task.cancelled { task.completionHandler(nil, nil, Error.cancelled) } else { - task.progressHandler(Progress(totalUnitCount: 1)) + task.uploadProgressHandler(Progress(totalUnitCount: 1)) + task.downloadProgressHandler(Progress(totalUnitCount: 1)) task.completionHandler(data, urlResponse, error) } } @@ -50,8 +51,8 @@ class TestSessionAdapter: SessionAdapter { tasks = [] } - func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { - let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler) + func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { + let task = TestSessionTask(uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler) tasks.append(task) return task diff --git a/Tests/APIKitTests/TestComponents/TestSessionTask.swift b/Tests/APIKitTests/TestComponents/TestSessionTask.swift index 05e3e92a..a6f4e658 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionTask.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionTask.swift @@ -4,16 +4,17 @@ import APIKit class TestSessionTask: SessionTask { var completionHandler: (Data?, URLResponse?, Error?) -> Void - var progressHandler: (Progress) -> Void + var uploadProgressHandler: Session.ProgressHandler + var downloadProgressHandler: Session.ProgressHandler var cancelled = false - init(progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { + init(uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { self.completionHandler = completionHandler - self.progressHandler = progressHandler + self.uploadProgressHandler = uploadProgressHandler + self.downloadProgressHandler = downloadProgressHandler } func resume() { - } func cancel() { From 2e3391983cf4b6a434decfd523a66810f956d48f Mon Sep 17 00:00:00 2001 From: Morgan Harris Date: Wed, 18 Jan 2017 09:44:42 +1100 Subject: [PATCH 11/18] Allow DataParser to return typed objects DataParser should not be required to return Any and have objects checked for type safety at runtime. This allows us to use the Swift build time type checking system with very minimal changes to existing code. The only API breaking change is that Request classes using the default JSONDataParser should now conform to JSONRequest, instead of just Request. This is because protocol extensions cannot declare default associated types. (cherry picked from commit 261af5fee62b133a452c798a6ad2c6996bb710ac) --- Sources/APIKit/DataParser/DataParser.swift | 4 +++- Sources/APIKit/Request.swift | 23 ++++++++++++------- Tests/APIKitTests/SessionTests.swift | 2 +- .../TestComponents/TestRequest.swift | 2 +- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Sources/APIKit/DataParser/DataParser.swift b/Sources/APIKit/DataParser/DataParser.swift index 304db092..7e0ffbc3 100644 --- a/Sources/APIKit/DataParser/DataParser.swift +++ b/Sources/APIKit/DataParser/DataParser.swift @@ -2,10 +2,12 @@ import Foundation /// `DataParser` protocol provides interface to parse HTTP response body and to state Content-Type to accept. public protocol DataParser { + associatedtype Parsed + /// Value for `Accept` header field of HTTP request. var contentType: String? { get } /// Return `Any` that expresses structure of response such as JSON and XML. /// - Throws: `Error` when parser encountered invalid format data. - func parse(data: Data) throws -> Any + func parse(data: Data) throws -> Parsed } diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request.swift index 8e7c02ae..a497daf9 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request.swift @@ -10,6 +10,7 @@ import Foundation public protocol Request { /// The response type associated with the request type. associatedtype Response + associatedtype Parser: DataParser /// The base URL. var baseURL: URL { get } @@ -40,7 +41,7 @@ public protocol Request { var headerFields: [String: String] { get } /// The parser object that states `Content-Type` to accept and parses response body. - var dataParser: DataParser { get } + var dataParser: Parser { get } /// Intercepts `URLRequest` which is created by `Request.buildURLRequest()`. If an error is /// thrown in this method, the result of `Session.send()` turns `.failure(.requestError(error))`. @@ -52,12 +53,12 @@ public protocol Request { /// The default implementation of this method is provided to throw `ResponseError.unacceptableStatusCode` /// if the HTTP status code is not in `200..<300`. /// - Throws: `Error` - func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any + func intercept(object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Parser.Parsed /// Build `Response` instance from raw response object. This method is called after /// `intercept(object:urlResponse:)` if it does not throw any error. /// - Throws: `Error` - func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response + func response(from object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Response } public extension Request { @@ -85,15 +86,11 @@ public extension Request { return [:] } - var dataParser: DataParser { - return JSONDataParser(readingOptions: []) - } - func intercept(urlRequest: URLRequest) throws -> URLRequest { return urlRequest } - func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any { + func intercept(object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Parser.Parsed { guard 200..<300 ~= urlResponse.statusCode else { throw ResponseError.unacceptableStatusCode(urlResponse.statusCode) } @@ -151,3 +148,13 @@ public extension Request where Response == Void { return } } + +public protocol JSONRequest: Request {} + +public extension JSONRequest { + + var dataParser: JSONDataParser { + return JSONDataParser(readingOptions: []) + } + +} diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index e2fd734e..8e4838d6 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -174,7 +174,7 @@ class SessionTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } - struct AnotherTestRequest: Request { + struct AnotherTestRequest: JSONRequest { typealias Response = Void var baseURL: URL { diff --git a/Tests/APIKitTests/TestComponents/TestRequest.swift b/Tests/APIKitTests/TestComponents/TestRequest.swift index e3bc69aa..32cd5317 100644 --- a/Tests/APIKitTests/TestComponents/TestRequest.swift +++ b/Tests/APIKitTests/TestComponents/TestRequest.swift @@ -1,7 +1,7 @@ import Foundation import APIKit -struct TestRequest: Request { +struct TestRequest: JSONRequest { var absoluteURL: URL? { let urlRequest = try? buildURLRequest() return urlRequest?.url From 4142c142e7098598260c29220660981357b3bd67 Mon Sep 17 00:00:00 2001 From: Yosuke Ishikawa Date: Tue, 11 Apr 2017 05:17:16 +0900 Subject: [PATCH 12/18] Rename Request.Parser to Request.DataParser (cherry picked from commit 3593a025340b9ec3518480f97a3f0bc2cd41a58c) --- Sources/APIKit/Request.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request.swift index a497daf9..14e7af53 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request.swift @@ -10,7 +10,7 @@ import Foundation public protocol Request { /// The response type associated with the request type. associatedtype Response - associatedtype Parser: DataParser + associatedtype DataParser: APIKit.DataParser /// The base URL. var baseURL: URL { get } @@ -41,7 +41,7 @@ public protocol Request { var headerFields: [String: String] { get } /// The parser object that states `Content-Type` to accept and parses response body. - var dataParser: Parser { get } + var dataParser: DataParser { get } /// Intercepts `URLRequest` which is created by `Request.buildURLRequest()`. If an error is /// thrown in this method, the result of `Session.send()` turns `.failure(.requestError(error))`. @@ -53,12 +53,12 @@ public protocol Request { /// The default implementation of this method is provided to throw `ResponseError.unacceptableStatusCode` /// if the HTTP status code is not in `200..<300`. /// - Throws: `Error` - func intercept(object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Parser.Parsed + func intercept(object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> DataParser.Parsed /// Build `Response` instance from raw response object. This method is called after /// `intercept(object:urlResponse:)` if it does not throw any error. /// - Throws: `Error` - func response(from object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Response + func response(from object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> Response } public extension Request { @@ -90,7 +90,7 @@ public extension Request { return urlRequest } - func intercept(object: Parser.Parsed, urlResponse: HTTPURLResponse) throws -> Parser.Parsed { + func intercept(object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> DataParser.Parsed { guard 200..<300 ~= urlResponse.statusCode else { throw ResponseError.unacceptableStatusCode(urlResponse.statusCode) } From 0d8c5cd4d7b536ba559515090281762a1d4ec147 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 22 Oct 2022 17:23:21 +0900 Subject: [PATCH 13/18] Fix playground build error --- Demo.playground/Contents.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 18a73aca..99d94a00 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -5,9 +5,7 @@ import APIKit PlaygroundPage.current.needsIndefiniteExecution = true //: Step 1: Define request protocol -protocol GitHubRequest: Request { - -} +protocol GitHubRequest: JSONRequest {} extension GitHubRequest { var baseURL: URL { From eb29cb635fd17983f6984ab8e1ba2982c96d18f7 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 22 Oct 2022 17:32:11 +0900 Subject: [PATCH 14/18] Move Request to a different directory --- APIKit.xcodeproj/project.pbxproj | 21 +++++++++++++++++---- Sources/APIKit/Request/JSONRequest.swift | 9 +++++++++ Sources/APIKit/{ => Request}/Request.swift | 10 ---------- 3 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 Sources/APIKit/Request/JSONRequest.swift rename Sources/APIKit/{ => Request}/Request.swift (97%) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 4b034458..ad240527 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 7F7048CD1D9D89BE003C99F6 /* APIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F7048C61D9D89BE003C99F6 /* APIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7F7048CE1D9D89BE003C99F6 /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */; }; 7F7048CF1D9D89BE003C99F6 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */; }; - 7F7048D11D9D89BE003C99F6 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CA1D9D89BE003C99F6 /* Request.swift */; }; 7F7048D21D9D89BE003C99F6 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CB1D9D89BE003C99F6 /* Session.swift */; }; 7F7048D31D9D89BE003C99F6 /* Unavailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */; }; 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */; }; @@ -49,6 +48,8 @@ 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; }; C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; }; C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; + C5F9A3482903E138000CB6C4 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3472903E138000CB6C4 /* Request.swift */; }; + C5F9A34A2903E147000CB6C4 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3492903E147000CB6C4 /* JSONRequest.swift */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; C5FF1DCF28A835600059573D /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCD28A835600059573D /* QueryParameters.swift */; }; C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */; }; @@ -112,7 +113,6 @@ 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = APIKit/CallbackQueue.swift; sourceTree = ""; }; 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = APIKit/HTTPMethod.swift; sourceTree = ""; }; 7F7048C91D9D89BE003C99F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = APIKit/Info.plist; sourceTree = ""; }; - 7F7048CA1D9D89BE003C99F6 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Request.swift; path = APIKit/Request.swift; sourceTree = ""; }; 7F7048CB1D9D89BE003C99F6 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = APIKit/Session.swift; sourceTree = ""; }; 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Unavailable.swift; path = APIKit/Unavailable.swift; sourceTree = ""; }; 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionAdapter.swift; path = Sources/APIKit/SessionAdapter/SessionAdapter.swift; sourceTree = SOURCE_ROOT; }; @@ -135,6 +135,8 @@ 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStub.swift; sourceTree = ""; }; C5725F4A28D8C36500810D7C /* Concurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = ""; }; C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; + C5F9A3472903E138000CB6C4 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + C5F9A3492903E147000CB6C4 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; C5FF1DCD28A835600059573D /* QueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; }; C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParameters.swift; sourceTree = ""; }; @@ -319,11 +321,11 @@ children = ( 7F7048C71D9D89BE003C99F6 /* CallbackQueue.swift */, 7F7048C81D9D89BE003C99F6 /* HTTPMethod.swift */, - 7F7048CA1D9D89BE003C99F6 /* Request.swift */, 7F7048CB1D9D89BE003C99F6 /* Session.swift */, 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */, C5725F4928D8C36500810D7C /* Concurrency */, 0969AE0D259DEC3C00C498AF /* Combine */, + C5F9A3462903E125000CB6C4 /* Request */, 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, C5FF1DCC28A835600059573D /* QueryParameters */, 7F18BD0D1C972C38003A31DF /* BodyParameters */, @@ -395,6 +397,16 @@ path = Concurrency; sourceTree = ""; }; + C5F9A3462903E125000CB6C4 /* Request */ = { + isa = PBXGroup; + children = ( + C5F9A3472903E138000CB6C4 /* Request.swift */, + C5F9A3492903E147000CB6C4 /* JSONRequest.swift */, + ); + name = Request; + path = APIKit/Request; + sourceTree = ""; + }; C5FF1DBF28A80FFD0059573D /* Resources */ = { isa = PBXGroup; children = ( @@ -533,9 +545,9 @@ buildActionMask = 2147483647; files = ( 7F7048D31D9D89BE003C99F6 /* Unavailable.swift in Sources */, - 7F7048D11D9D89BE003C99F6 /* Request.swift in Sources */, 7F7048E81D9D8A08003C99F6 /* DataParser.swift in Sources */, 7F7048CE1D9D89BE003C99F6 /* CallbackQueue.swift in Sources */, + C5F9A34A2903E147000CB6C4 /* JSONRequest.swift in Sources */, 7F7048DE1D9D89FB003C99F6 /* AbstractInputStream.m in Sources */, 7F7048E31D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift in Sources */, 7F7048F01D9D8A12003C99F6 /* ResponseError.swift in Sources */, @@ -549,6 +561,7 @@ 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */, 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */, 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */, + C5F9A3482903E138000CB6C4 /* Request.swift in Sources */, ECA8314C1DE4E677004EB1B5 /* ProtobufBodyParameters.swift in Sources */, C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */, 7F7048E11D9D89FB003C99F6 /* FormURLEncodedBodyParameters.swift in Sources */, diff --git a/Sources/APIKit/Request/JSONRequest.swift b/Sources/APIKit/Request/JSONRequest.swift new file mode 100644 index 00000000..b19508fb --- /dev/null +++ b/Sources/APIKit/Request/JSONRequest.swift @@ -0,0 +1,9 @@ +import Foundation + +public protocol JSONRequest: Request {} + +public extension JSONRequest { + var dataParser: JSONDataParser { + return JSONDataParser(readingOptions: []) + } +} diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request/Request.swift similarity index 97% rename from Sources/APIKit/Request.swift rename to Sources/APIKit/Request/Request.swift index 14e7af53..208a459a 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request/Request.swift @@ -148,13 +148,3 @@ public extension Request where Response == Void { return } } - -public protocol JSONRequest: Request {} - -public extension JSONRequest { - - var dataParser: JSONDataParser { - return JSONDataParser(readingOptions: []) - } - -} From 691c7815736c99ec7e6b2dbb81b9ee271200e235 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 22 Oct 2022 17:33:42 +0900 Subject: [PATCH 15/18] Add APIKit 6 migration guide --- Documentation/APIKit6MigrationGuide.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 Documentation/APIKit6MigrationGuide.md diff --git a/Documentation/APIKit6MigrationGuide.md b/Documentation/APIKit6MigrationGuide.md new file mode 100644 index 00000000..85197181 --- /dev/null +++ b/Documentation/APIKit6MigrationGuide.md @@ -0,0 +1 @@ +# APIKit 6 Migration Guide \ No newline at end of file From c8ca8b38e7bc15e600f4c8b183f0932f393fa7a3 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 23 Oct 2022 14:47:17 +0900 Subject: [PATCH 16/18] Add types to DataParser response --- .../DataParser/FormURLEncodedDataParser.swift | 4 ++-- Sources/APIKit/DataParser/ProtobufDataParser.swift | 4 ++-- Sources/APIKit/DataParser/StringDataParser.swift | 2 +- Sources/APIKit/Request/Request.swift | 2 +- .../FormURLEncodedDataParserTests.swift | 7 +++---- .../DataParserType/JSONDataParserTests.swift | 14 +++++++++++++- .../DataParserType/ProtobufDataParserTests.swift | 2 +- .../DataParserType/StringDataParserTests.swift | 2 +- 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Sources/APIKit/DataParser/FormURLEncodedDataParser.swift b/Sources/APIKit/DataParser/FormURLEncodedDataParser.swift index fc46457e..34182f38 100644 --- a/Sources/APIKit/DataParser/FormURLEncodedDataParser.swift +++ b/Sources/APIKit/DataParser/FormURLEncodedDataParser.swift @@ -21,9 +21,9 @@ public class FormURLEncodedDataParser: DataParser { return "application/x-www-form-urlencoded" } - /// Return `Any` that expresses structure of response. + /// Return `[String: Any]` that expresses structure of response. /// - Throws: `FormURLEncodedDataParser.Error` when the parser fails to initialize `String` from `Data`. - public func parse(data: Data) throws -> Any { + public func parse(data: Data) throws -> [String: Any] { guard let string = String(data: data, encoding: encoding) else { throw Error.cannotGetStringFromData(data) } diff --git a/Sources/APIKit/DataParser/ProtobufDataParser.swift b/Sources/APIKit/DataParser/ProtobufDataParser.swift index 7e2a6d47..a140077e 100644 --- a/Sources/APIKit/DataParser/ProtobufDataParser.swift +++ b/Sources/APIKit/DataParser/ProtobufDataParser.swift @@ -12,8 +12,8 @@ public class ProtobufDataParser: DataParser { return "application/protobuf" } - /// Return `Any` that expresses structure of Data response. - public func parse(data: Data) throws -> Any { + /// Return `Data` that expresses structure of Data response. + public func parse(data: Data) throws -> Data { return data } } diff --git a/Sources/APIKit/DataParser/StringDataParser.swift b/Sources/APIKit/DataParser/StringDataParser.swift index 6ae648f6..48d0994d 100644 --- a/Sources/APIKit/DataParser/StringDataParser.swift +++ b/Sources/APIKit/DataParser/StringDataParser.swift @@ -23,7 +23,7 @@ public class StringDataParser: DataParser { /// Return `String` that converted from `Data`. /// - Throws: `StringDataParser.Error` when the parser fails to initialize `String` from `Data`. - public func parse(data: Data) throws -> Any { + public func parse(data: Data) throws -> String { guard let string = String(data: data, encoding: encoding) else { throw Error.invalidData(data) } diff --git a/Sources/APIKit/Request/Request.swift b/Sources/APIKit/Request/Request.swift index 208a459a..1a2ff4fb 100644 --- a/Sources/APIKit/Request/Request.swift +++ b/Sources/APIKit/Request/Request.swift @@ -144,7 +144,7 @@ public extension Request { } public extension Request where Response == Void { - func response(from object: Any, urlResponse: HTTPURLResponse) throws { + func response(from object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws { return } } diff --git a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift index cb2f5357..359e267f 100644 --- a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift @@ -14,10 +14,9 @@ class FormURLEncodedDataParserTests: XCTestCase { let parser = FormURLEncodedDataParser(encoding: .utf8) let object = try parser.parse(data: data) - let dictionary = object as? [String: String] - XCTAssertEqual(dictionary?["foo"], "1") - XCTAssertEqual(dictionary?["bar"], "2") - XCTAssertEqual(dictionary?["baz"], "3") + XCTAssertEqual(object["foo"] as? String, "1") + XCTAssertEqual(object["bar"] as? String, "2") + XCTAssertEqual(object["baz"] as? String, "3") } func testInvalidString() { diff --git a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift index 3cf81427..312f92d7 100644 --- a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift @@ -8,7 +8,7 @@ class JSONDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/json") } - func testJSONSuccess() throws { + func testDictionaryJSONSuccess() throws { let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = JSONDataParser(readingOptions: []) @@ -19,4 +19,16 @@ class JSONDataParserTests: XCTestCase { XCTAssertEqual(dictionary?["bar"], 2) XCTAssertEqual(dictionary?["baz"], 3) } + + func testArrayJSONSuccess() throws { + let string = "[1, 2, 3]" + let data = string.data(using: .utf8, allowLossyConversion: false)! + let parser = JSONDataParser(readingOptions: []) + + let object = try parser.parse(data: data) + let array = object as? [Int] + XCTAssertEqual(array?[0], 1) + XCTAssertEqual(array?[1], 2) + XCTAssertEqual(array?[2], 3) + } } diff --git a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift index 63e91853..a175d5fe 100644 --- a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift @@ -12,7 +12,7 @@ class ProtobufDataParserTests: XCTestCase { let data = try XCTUnwrap("data".data(using: .utf8)) let parser = ProtobufDataParser() - let object = try XCTUnwrap(try parser.parse(data: data) as? Data) + let object = try parser.parse(data: data) let string = String(data: object, encoding: .utf8) XCTAssertEqual(string, "data") } diff --git a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift index d932501a..bc4a597d 100644 --- a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift @@ -14,7 +14,7 @@ class StringDataParserTests: XCTestCase { let parser = StringDataParser(encoding: .utf8) let object = try parser.parse(data: data) - XCTAssertEqual(object as? String, string) + XCTAssertEqual(object, string) } func testInvalidString() { From f9651100974f982977867a75688acf5ed192779f Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 23 Oct 2022 14:53:59 +0900 Subject: [PATCH 17/18] Add `DecodableJSONDataParser` for `Foundation.Decodable` --- APIKit.xcodeproj/project.pbxproj | 8 +++ Demo.playground/Contents.swift | 49 +++++++++---------- .../DataParser/DecodableJSONDataParser.swift | 19 +++++++ .../DecodableJSONDataParserTests.swift | 18 +++++++ 4 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 Sources/APIKit/DataParser/DecodableJSONDataParser.swift create mode 100644 Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index ad240527..9b64487e 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; C5F9A3482903E138000CB6C4 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3472903E138000CB6C4 /* Request.swift */; }; C5F9A34A2903E147000CB6C4 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3492903E147000CB6C4 /* JSONRequest.swift */; }; + C5F9A34C2905073D000CB6C4 /* DecodableJSONDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */; }; + C5F9A34E29050A96000CB6C4 /* DecodableJSONDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; C5FF1DCF28A835600059573D /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCD28A835600059573D /* QueryParameters.swift */; }; C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */; }; @@ -137,6 +139,8 @@ C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; C5F9A3472903E138000CB6C4 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; C5F9A3492903E147000CB6C4 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; + C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableJSONDataParser.swift; sourceTree = ""; }; + C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableJSONDataParserTests.swift; sourceTree = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; C5FF1DCD28A835600059573D /* QueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; }; C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParameters.swift; sourceTree = ""; }; @@ -282,6 +286,7 @@ children = ( 7F698E411D9D680C00F1561D /* FormURLEncodedDataParserTests.swift */, 7F698E421D9D680C00F1561D /* JSONDataParserTests.swift */, + C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */, ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */, 7F698E431D9D680C00F1561D /* StringDataParserTests.swift */, ); @@ -373,6 +378,7 @@ 7F7048E41D9D8A08003C99F6 /* DataParser.swift */, 7F7048E51D9D8A08003C99F6 /* FormURLEncodedDataParser.swift */, 7F7048E61D9D8A08003C99F6 /* JSONDataParser.swift */, + C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */, ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */, 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */, ); @@ -568,6 +574,7 @@ 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */, ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */, + C5F9A34C2905073D000CB6C4 /* DecodableJSONDataParser.swift in Sources */, 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */, 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */, 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */, @@ -589,6 +596,7 @@ C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, + C5F9A34E29050A96000CB6C4 /* DecodableJSONDataParserTests.swift in Sources */, C5FF1DD328A835680059573D /* URLEncodedQueryParametersTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 99d94a00..46f1137d 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -5,30 +5,37 @@ import APIKit PlaygroundPage.current.needsIndefiniteExecution = true //: Step 1: Define request protocol -protocol GitHubRequest: JSONRequest {} +protocol GitHubRequest: Request {} extension GitHubRequest { var baseURL: URL { return URL(string: "https://api.github.com")! } + + var dataParser: DecodableJSONDataParser { + return DecodableJSONDataParser() + } } //: Step 2: Create model object -struct RateLimit { +struct RateLimit: Decodable { let count: Int let resetDate: Date - init?(dictionary: [String: AnyObject]) { - guard let count = dictionary["rate"]?["limit"] as? Int else { - return nil - } - - guard let resetDateString = dictionary["rate"]?["reset"] as? TimeInterval else { - return nil - } + enum CodingKeys: String, CodingKey { + case rate + } + enum RateCodingKeys: String, CodingKey { + case limit + case reset + } - self.count = count - self.resetDate = Date(timeIntervalSince1970: resetDateString) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let rateContainer = try container.nestedContainer(keyedBy: RateCodingKeys.self, forKey: .rate) + self.count = try rateContainer.decode(Int.self, forKey: .limit) + let resetTimeInterval = try rateContainer.decode(TimeInterval.self, forKey: .reset) + self.resetDate = Date(timeIntervalSince1970: resetTimeInterval) } } @@ -37,21 +44,11 @@ struct RateLimit { struct GetRateLimitRequest: GitHubRequest { typealias Response = RateLimit - var method: HTTPMethod { - return .get - } - - var path: String { - return "/rate_limit" - } - - func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { - guard let dictionary = object as? [String: AnyObject], - let rateLimit = RateLimit(dictionary: dictionary) else { - throw ResponseError.unexpectedObject(object) - } + let method: HTTPMethod = .get + let path: String = "/rate_limit" - return rateLimit + func response(from object: Data, urlResponse: HTTPURLResponse) throws -> Response { + return try JSONDecoder().decode(Response.self, from: object) } } diff --git a/Sources/APIKit/DataParser/DecodableJSONDataParser.swift b/Sources/APIKit/DataParser/DecodableJSONDataParser.swift new file mode 100644 index 00000000..28ee9ec3 --- /dev/null +++ b/Sources/APIKit/DataParser/DecodableJSONDataParser.swift @@ -0,0 +1,19 @@ +import Foundation + +/// `DecodableJSONDataParser` response Data data. +public class DecodableJSONDataParser: DataParser { + /// Returns `DecodableJSONDataParser`. + public init() {} + + // MARK: - DataParser + + /// Value for `Accept` header field of HTTP request. + public var contentType: String? { + return "application/json" + } + + /// Return `Data` that expresses structure of Data response. + public func parse(data: Data) throws -> Data { + return data + } +} diff --git a/Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift b/Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift new file mode 100644 index 00000000..0e0fea1f --- /dev/null +++ b/Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift @@ -0,0 +1,18 @@ +import XCTest +import APIKit +import XCTest + +class DecodableJSONDataParserTests: XCTestCase { + func testContentType() { + let parser = DecodableJSONDataParser() + XCTAssertEqual(parser.contentType, "application/json") + } + + func testJSONSuccess() throws { + let data = try XCTUnwrap("data".data(using: .utf8)) + let parser = DecodableJSONDataParser() + + let object = try parser.parse(data: data) + XCTAssertEqual(object, data) + } +} From 58fc67ca86a4c2fa2afa4d35e1aaebeba97818f7 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 23 Oct 2022 15:23:35 +0900 Subject: [PATCH 18/18] Rename DecodableJSONDataParser to NonSerializedJSONDataParser --- APIKit.xcodeproj/project.pbxproj | 16 ++++++++-------- Demo.playground/Contents.swift | 4 ++-- ...r.swift => NonSerializedJSONDataParser.swift} | 6 +++--- ...ft => NonSerializedJSONDataParserTests.swift} | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) rename Sources/APIKit/DataParser/{DecodableJSONDataParser.swift => NonSerializedJSONDataParser.swift} (69%) rename Tests/APIKitTests/DataParserType/{DecodableJSONDataParserTests.swift => NonSerializedJSONDataParserTests.swift} (68%) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 9b64487e..816644ba 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -50,8 +50,8 @@ C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; }; C5F9A3482903E138000CB6C4 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3472903E138000CB6C4 /* Request.swift */; }; C5F9A34A2903E147000CB6C4 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A3492903E147000CB6C4 /* JSONRequest.swift */; }; - C5F9A34C2905073D000CB6C4 /* DecodableJSONDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */; }; - C5F9A34E29050A96000CB6C4 /* DecodableJSONDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */; }; + C5F9A34C2905073D000CB6C4 /* NonSerializedJSONDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34B2905073D000CB6C4 /* NonSerializedJSONDataParser.swift */; }; + C5F9A34E29050A96000CB6C4 /* NonSerializedJSONDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F9A34D29050A96000CB6C4 /* NonSerializedJSONDataParserTests.swift */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; C5FF1DCF28A835600059573D /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCD28A835600059573D /* QueryParameters.swift */; }; C5FF1DD028A835600059573D /* URLEncodedQueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */; }; @@ -139,8 +139,8 @@ C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; C5F9A3472903E138000CB6C4 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; C5F9A3492903E147000CB6C4 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; - C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableJSONDataParser.swift; sourceTree = ""; }; - C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableJSONDataParserTests.swift; sourceTree = ""; }; + C5F9A34B2905073D000CB6C4 /* NonSerializedJSONDataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSerializedJSONDataParser.swift; sourceTree = ""; }; + C5F9A34D29050A96000CB6C4 /* NonSerializedJSONDataParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSerializedJSONDataParserTests.swift; sourceTree = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; C5FF1DCD28A835600059573D /* QueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; }; C5FF1DCE28A835600059573D /* URLEncodedQueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParameters.swift; sourceTree = ""; }; @@ -286,7 +286,7 @@ children = ( 7F698E411D9D680C00F1561D /* FormURLEncodedDataParserTests.swift */, 7F698E421D9D680C00F1561D /* JSONDataParserTests.swift */, - C5F9A34D29050A96000CB6C4 /* DecodableJSONDataParserTests.swift */, + C5F9A34D29050A96000CB6C4 /* NonSerializedJSONDataParserTests.swift */, ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */, 7F698E431D9D680C00F1561D /* StringDataParserTests.swift */, ); @@ -378,7 +378,7 @@ 7F7048E41D9D8A08003C99F6 /* DataParser.swift */, 7F7048E51D9D8A08003C99F6 /* FormURLEncodedDataParser.swift */, 7F7048E61D9D8A08003C99F6 /* JSONDataParser.swift */, - C5F9A34B2905073D000CB6C4 /* DecodableJSONDataParser.swift */, + C5F9A34B2905073D000CB6C4 /* NonSerializedJSONDataParser.swift */, ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */, 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */, ); @@ -574,7 +574,7 @@ 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */, ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */, - C5F9A34C2905073D000CB6C4 /* DecodableJSONDataParser.swift in Sources */, + C5F9A34C2905073D000CB6C4 /* NonSerializedJSONDataParser.swift in Sources */, 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */, 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */, 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */, @@ -596,7 +596,7 @@ C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, - C5F9A34E29050A96000CB6C4 /* DecodableJSONDataParserTests.swift in Sources */, + C5F9A34E29050A96000CB6C4 /* NonSerializedJSONDataParserTests.swift in Sources */, C5FF1DD328A835680059573D /* URLEncodedQueryParametersTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, diff --git a/Demo.playground/Contents.swift b/Demo.playground/Contents.swift index 46f1137d..c18ec928 100644 --- a/Demo.playground/Contents.swift +++ b/Demo.playground/Contents.swift @@ -12,8 +12,8 @@ extension GitHubRequest { return URL(string: "https://api.github.com")! } - var dataParser: DecodableJSONDataParser { - return DecodableJSONDataParser() + var dataParser: NonSerializedJSONDataParser { + return NonSerializedJSONDataParser() } } diff --git a/Sources/APIKit/DataParser/DecodableJSONDataParser.swift b/Sources/APIKit/DataParser/NonSerializedJSONDataParser.swift similarity index 69% rename from Sources/APIKit/DataParser/DecodableJSONDataParser.swift rename to Sources/APIKit/DataParser/NonSerializedJSONDataParser.swift index 28ee9ec3..6e8b5096 100644 --- a/Sources/APIKit/DataParser/DecodableJSONDataParser.swift +++ b/Sources/APIKit/DataParser/NonSerializedJSONDataParser.swift @@ -1,8 +1,8 @@ import Foundation -/// `DecodableJSONDataParser` response Data data. -public class DecodableJSONDataParser: DataParser { - /// Returns `DecodableJSONDataParser`. +/// `NonSerializedJSONDataParser` response Data data. +public class NonSerializedJSONDataParser: DataParser { + /// Returns `NonSerializedJSONDataParser`. public init() {} // MARK: - DataParser diff --git a/Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift b/Tests/APIKitTests/DataParserType/NonSerializedJSONDataParserTests.swift similarity index 68% rename from Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift rename to Tests/APIKitTests/DataParserType/NonSerializedJSONDataParserTests.swift index 0e0fea1f..48451dd4 100644 --- a/Tests/APIKitTests/DataParserType/DecodableJSONDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/NonSerializedJSONDataParserTests.swift @@ -2,15 +2,15 @@ import XCTest import APIKit import XCTest -class DecodableJSONDataParserTests: XCTestCase { +class NonSerializedJSONDataParserTests: XCTestCase { func testContentType() { - let parser = DecodableJSONDataParser() + let parser = NonSerializedJSONDataParser() XCTAssertEqual(parser.contentType, "application/json") } func testJSONSuccess() throws { let data = try XCTUnwrap("data".data(using: .utf8)) - let parser = DecodableJSONDataParser() + let parser = NonSerializedJSONDataParser() let object = try parser.parse(data: data) XCTAssertEqual(object, data)