From 8521bf047937ea639d64f0f3e79dbd00f042e3ec Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Tue, 24 Sep 2019 17:43:37 +0900 Subject: [PATCH 01/31] ExpressibleByDictionaryLiteral for JSONBodyParameters --- .../BodyParameters/JSONBodyParameters.swift | 9 ++++++++ .../JSONBodyParametersTests.swift | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Sources/APIKit/BodyParameters/JSONBodyParameters.swift b/Sources/APIKit/BodyParameters/JSONBodyParameters.swift index 3f541917..4388ce34 100644 --- a/Sources/APIKit/BodyParameters/JSONBodyParameters.swift +++ b/Sources/APIKit/BodyParameters/JSONBodyParameters.swift @@ -32,3 +32,12 @@ public struct JSONBodyParameters: BodyParameters { return .data(try JSONSerialization.data(withJSONObject: JSONObject, options: writingOptions)) } } + +extension JSONBodyParameters: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = Any + + public init(dictionaryLiteral elements: (Key, Value)...) { + self.init(JSONObject: Dictionary(uniqueKeysWithValues: elements)) + } +} diff --git a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift index 006c8e04..512eb749 100644 --- a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift @@ -36,4 +36,25 @@ class JSONBodyParametersTests: XCTestCase { XCTAssertEqual(nserror.code, 3840) } } + + func testDictionaryLiteral() { + let object = ["foo": 1, "bar": 2, "baz": 3] + + let parameters1: JSONBodyParameters = .init(JSONObject: object) + let parameters2: JSONBodyParameters = ["foo": 1, "bar": 2, "baz": 3] + do { + guard case .data(let data1) = try parameters1.buildEntity(), + case .data(let data2) = try parameters2.buildEntity() else { + XCTFail() + return + } + let dictionary1 = try JSONSerialization.jsonObject(with: data1, options: []) + let dictionary2 = try JSONSerialization.jsonObject(with: data2, options: []) + XCTAssertEqual((dictionary1 as? [String: Int])?["foo"], (dictionary2 as? [String: Int])?["foo"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["bar"], (dictionary2 as? [String: Int])?["bar"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["baz"], (dictionary2 as? [String: Int])?["baz"]) + } catch { + XCTFail() + } + } } From a1f8917c671d3f8de74e9850eb7b8577feafeb95 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Thu, 31 Dec 2020 21:44:05 +0900 Subject: [PATCH 02/31] Add support for Combine.framework --- APIKit.xcodeproj/project.pbxproj | 13 +++++ Sources/APIKit/Combine/Combine.swift | 86 ++++++++++++++++++++++++++++ Sources/APIKit/Session.swift | 2 +- 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 Sources/APIKit/Combine/Combine.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index b538939e..17dc7182 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969AE0E259DEC6D00C498AF /* Combine.swift */; }; 7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */; }; 7F698E511D9D680C00F1561D /* JSONBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */; }; 7F698E521D9D680C00F1561D /* MultipartFormDataParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */; }; @@ -76,6 +77,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0969AE0E259DEC6D00C498AF /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = ""; }; 141F120F1C1C96820026D415 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configurations/Base.xcconfig; sourceTree = ""; }; 141F12101C1C96820026D415 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = ""; }; 141F12111C1C96820026D415 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; @@ -149,6 +151,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0969AE0D259DEC3C00C498AF /* Combine */ = { + isa = PBXGroup; + children = ( + 0969AE0E259DEC6D00C498AF /* Combine.swift */, + ); + name = Combine; + path = APIKit/Combine; + sourceTree = ""; + }; 141F120E1C1C96690026D415 /* Configurations */ = { isa = PBXGroup; children = ( @@ -288,6 +299,7 @@ 7F7048CA1D9D89BE003C99F6 /* Request.swift */, 7F7048CB1D9D89BE003C99F6 /* Session.swift */, 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */, + 0969AE0D259DEC3C00C498AF /* Combine */, 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, 7F18BD0D1C972C38003A31DF /* BodyParameters */, 7FA19A441C9CC9A2005D25AE /* DataParser */, @@ -473,6 +485,7 @@ ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */, 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */, 7F7048D71D9D89F2003C99F6 /* URLSessionAdapter.swift in Sources */, + 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */, 7F7048EB1D9D8A08003C99F6 /* StringDataParser.swift in Sources */, 7F7048CF1D9D89BE003C99F6 /* HTTPMethod.swift in Sources */, ); diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift new file mode 100644 index 00000000..5f11a00e --- /dev/null +++ b/Sources/APIKit/Combine/Combine.swift @@ -0,0 +1,86 @@ +#if canImport(Combine) + +import Foundation +import Combine + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct RequestPublisher: Publisher { + public typealias Output = Request.Response + public typealias Failure = SessionTaskError + + private let session: Session + private let request: Request + private let callbackQueue: CallbackQueue? + + public init(session: Session, request: Request, callbackQueue: CallbackQueue?) { + self.session = session + self.request = request + self.callbackQueue = callbackQueue + } + + public func receive(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { + subscriber.receive(subscription: RequestSubscription(session: session, + request: request, + callbackQueue: callbackQueue, + downstream: subscriber)) + } + + private final class RequestSubscription: Subscription where Downstream.Input == Request.Response, Downstream.Failure == Failure { + + private let session: Session + private let request: Request + private let callbackQueue: CallbackQueue? + private var downstream: Downstream? + private var task: SessionTask? + + init(session: Session, request: Request, callbackQueue: CallbackQueue?, downstream: Downstream) { + self.session = session + self.request = request + self.callbackQueue = callbackQueue + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + guard let downstream = self.downstream else { return } + self.downstream = nil + task = session.send(request, callbackQueue: callbackQueue) { result in + switch result { + case .success(let response): + _ = downstream.receive(response) + downstream.receive(completion: .finished) + case .failure(let error): + downstream.receive(completion: .failure(error)) + } + } + } + + func cancel() { + task?.cancel() + downstream = nil + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public extension Session { + /// Calls `publisher(_:callbackQueue:)` of `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. + /// - returns: The new request publisher. + static func publisher(_ request: Request, callbackQueue: CallbackQueue? = nil) -> RequestPublisher { + return RequestPublisher(session: .shared, request: request, callbackQueue: callbackQueue) + } + + /// Returns a publisher that wraps a task for a given `Request`. + /// + /// The publisher publishes `Request.Response` when the task completes, or terminates if the task fails with an error. + /// - 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. + /// - returns: The new request publisher. + func publisher(_ request: Request, callbackQueue: CallbackQueue? = nil) -> RequestPublisher { + return RequestPublisher(session: self, request: request, callbackQueue: callbackQueue) + } +} + +#endif diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index c772247f..a3e14d07 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -30,7 +30,7 @@ open class Session { return privateShared } - /// Calls `send(_:handler:)` of `sharedSession`. + /// Calls `send(_:callbackQueue:handler:)` of `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. From 8e8c2774e29da87da3810ded4144bdfc55783f55 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Fri, 1 Jan 2021 00:36:10 +0900 Subject: [PATCH 03/31] fix compile error --- Sources/APIKit/Combine/Combine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift index 5f11a00e..617e2228 100644 --- a/Sources/APIKit/Combine/Combine.swift +++ b/Sources/APIKit/Combine/Combine.swift @@ -18,7 +18,7 @@ public struct RequestPublisher: Publisher { self.callbackQueue = callbackQueue } - public func receive(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { + public func receive(subscriber: S) where S : Subscriber, RequestPublisher.Failure == S.Failure, RequestPublisher.Output == S.Input { subscriber.receive(subscription: RequestSubscription(session: session, request: request, callbackQueue: callbackQueue, From d2d512b5ac357146daa7c17869443050eb0979ad Mon Sep 17 00:00:00 2001 From: Econa77 Date: Fri, 1 Jan 2021 01:25:40 +0900 Subject: [PATCH 04/31] Add tests --- APIKit.xcodeproj/project.pbxproj | 12 +++ Tests/APIKitTests/Combine/CombineTests.swift | 95 ++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 Tests/APIKitTests/Combine/CombineTests.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 17dc7182..8fc0f748 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969AE0E259DEC6D00C498AF /* Combine.swift */; }; + 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0973EE34259E2DDC00879BA2 /* CombineTests.swift */; }; 7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */; }; 7F698E511D9D680C00F1561D /* JSONBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3D1D9D680C00F1561D /* JSONBodyParametersTests.swift */; }; 7F698E521D9D680C00F1561D /* MultipartFormDataParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3E1D9D680C00F1561D /* MultipartFormDataParametersTests.swift */; }; @@ -78,6 +79,7 @@ /* Begin PBXFileReference section */ 0969AE0E259DEC6D00C498AF /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = ""; }; + 0973EE34259E2DDC00879BA2 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; 141F120F1C1C96820026D415 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configurations/Base.xcconfig; sourceTree = ""; }; 141F12101C1C96820026D415 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = ""; }; 141F12111C1C96820026D415 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; @@ -160,6 +162,14 @@ path = APIKit/Combine; sourceTree = ""; }; + 0973EE33259E2DD000879BA2 /* Combine */ = { + isa = PBXGroup; + children = ( + 0973EE34259E2DDC00879BA2 /* CombineTests.swift */, + ); + path = Combine; + sourceTree = ""; + }; 141F120E1C1C96690026D415 /* Configurations */ = { isa = PBXGroup; children = ( @@ -230,6 +240,7 @@ 7F698E451D9D680C00F1561D /* RequestTests.swift */, 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */, 7F698E4A1D9D680C00F1561D /* SessionTests.swift */, + 0973EE33259E2DD000879BA2 /* Combine */, 7F698E3B1D9D680C00F1561D /* BodyParametersType */, 7F698E401D9D680C00F1561D /* DataParserType */, 7F698E461D9D680C00F1561D /* SessionAdapterType */, @@ -503,6 +514,7 @@ ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */, 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, + 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */, 7F698E561D9D680C00F1561D /* StringDataParserTests.swift in Sources */, diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift new file mode 100644 index 00000000..812b0670 --- /dev/null +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -0,0 +1,95 @@ +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +import APIKit + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +final class CombineTests: XCTestCase { + + var adapter: TestSessionAdapter! + var session: Session! + var cancellables: Set = [] + + override func setUp() { + super.setUp() + adapter = TestSessionAdapter() + session = Session(adapter: adapter) + } + + override func tearDown() { + super.tearDown() + cancellables = [] + } + + func testSuccess() { + let dictionary = ["key": "value"] + adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest() + + session.publisher(request) + .sink { completion in + switch completion { + case .failure: + XCTFail() + case .finished: + expectation.fulfill() + } + } receiveValue: { response in + XCTAssertEqual((response as? [String: String])?["key"], "value") + } + .store(in: &cancellables) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testParseDataError() { + adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest() + + session.publisher(request) + .sink { completion in + if case .failure(let error) = completion, case .responseError(let responseError as NSError) = error { + XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) + XCTAssertEqual(responseError.code, 3840) + } else { + XCTFail() + } + expectation.fulfill() + } receiveValue: { response in + XCTFail() + } + .store(in: &cancellables) + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testCancel() { + let expectation = self.expectation(description: "wait for response") + let request = TestRequest() + + let cancellable = session.publisher(request) + .sink { completion in + if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { + XCTAssertEqual(connectionError.code, 0) + } else { + XCTFail() + } + expectation.fulfill() + } receiveValue: { response in + XCTFail() + } + + cancellable.cancel() + + waitForExpectations(timeout: 1.0, handler: nil) + } + +} + +#endif From ec4383a24055d780861a7a1903d7f09a214039ab Mon Sep 17 00:00:00 2001 From: Econa77 Date: Fri, 1 Jan 2021 01:31:16 +0900 Subject: [PATCH 05/31] fix compile error --- Tests/APIKitTests/Combine/CombineTests.swift | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift index 812b0670..809af613 100644 --- a/Tests/APIKitTests/Combine/CombineTests.swift +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -31,16 +31,16 @@ final class CombineTests: XCTestCase { let request = TestRequest() session.publisher(request) - .sink { completion in + .sink(receiveCompletion: { completion in switch completion { case .failure: XCTFail() case .finished: expectation.fulfill() } - } receiveValue: { response in + }, receiveValue: { response in XCTAssertEqual((response as? [String: String])?["key"], "value") - } + }) .store(in: &cancellables) waitForExpectations(timeout: 1.0, handler: nil) @@ -53,17 +53,17 @@ final class CombineTests: XCTestCase { let request = TestRequest() session.publisher(request) - .sink { completion in + .sink(receiveCompletion: { completion in if case .failure(let error) = completion, case .responseError(let responseError as NSError) = error { XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) XCTAssertEqual(responseError.code, 3840) + expectation.fulfill() } else { XCTFail() } - expectation.fulfill() - } receiveValue: { response in + }, receiveValue: { response in XCTFail() - } + }) .store(in: &cancellables) waitForExpectations(timeout: 1.0, handler: nil) @@ -74,16 +74,16 @@ final class CombineTests: XCTestCase { let request = TestRequest() let cancellable = session.publisher(request) - .sink { completion in + .sink(receiveCompletion: { completion in if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { XCTAssertEqual(connectionError.code, 0) + expectation.fulfill() } else { XCTFail() } - expectation.fulfill() - } receiveValue: { response in + }, receiveValue: { response in XCTFail() - } + }) cancellable.cancel() From 1dcee7d9a9a4d1031498c8b0d194cf306e3ad0c4 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Tue, 23 Feb 2021 16:14:58 +0900 Subject: [PATCH 06/31] Rename publisher() to sessionTaskPublisher(for:) --- Sources/APIKit/Combine/Combine.swift | 49 +++++++++++--------- Sources/APIKit/Session.swift | 4 +- Tests/APIKitTests/Combine/CombineTests.swift | 6 +-- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift index 617e2228..0b4db3f4 100644 --- a/Sources/APIKit/Combine/Combine.swift +++ b/Sources/APIKit/Combine/Combine.swift @@ -3,39 +3,42 @@ import Foundation import Combine -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) -public struct RequestPublisher: Publisher { +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public struct SessionTaskPublisher: Publisher { + /// The kind of values published by this publisher. public typealias Output = Request.Response + + /// The kind of errors this publisher might publish. public typealias Failure = SessionTaskError - private let session: Session private let request: Request + private let session: Session private let callbackQueue: CallbackQueue? - public init(session: Session, request: Request, callbackQueue: CallbackQueue?) { - self.session = session + public init(request: Request, session: Session, callbackQueue: CallbackQueue?) { self.request = request + self.session = session self.callbackQueue = callbackQueue } - public func receive(subscriber: S) where S : Subscriber, RequestPublisher.Failure == S.Failure, RequestPublisher.Output == S.Input { - subscriber.receive(subscription: RequestSubscription(session: session, - request: request, - callbackQueue: callbackQueue, - downstream: subscriber)) + public func receive(subscriber: S) where S: Subscriber, S.Failure == SessionTaskPublisher.Failure, S.Input == SessionTaskPublisher.Output { + subscriber.receive(subscription: SessionTaskSubscription(request: request, + session: session, + callbackQueue: callbackQueue, + downstream: subscriber)) } - private final class RequestSubscription: Subscription where Downstream.Input == Request.Response, Downstream.Failure == Failure { + private final class SessionTaskSubscription: Subscription where Request.Response == Downstream.Input, Downstream.Failure == Failure { - private let session: Session private let request: Request + private let session: Session private let callbackQueue: CallbackQueue? private var downstream: Downstream? private var task: SessionTask? - init(session: Session, request: Request, callbackQueue: CallbackQueue?, downstream: Downstream) { - self.session = session + init(request: Request, session: Session, callbackQueue: CallbackQueue?, downstream: Downstream) { self.request = request + self.session = session self.callbackQueue = callbackQueue self.downstream = downstream } @@ -62,24 +65,24 @@ public struct RequestPublisher: Publisher { } } -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Session { - /// Calls `publisher(_:callbackQueue:)` of `shared`. + /// Calls `sessionTaskPublisher(for:callbackQueue:)` 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. - /// - returns: The new request publisher. - static func publisher(_ request: Request, callbackQueue: CallbackQueue? = nil) -> RequestPublisher { - return RequestPublisher(session: .shared, request: request, callbackQueue: callbackQueue) + /// - returns: A publisher that wraps a session task for the request. + static func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { + return SessionTaskPublisher(request: request, session: .shared, callbackQueue: callbackQueue) } - /// Returns a publisher that wraps a task for a given `Request`. + /// Returns a publisher that wraps a session task for the request. /// /// The publisher publishes `Request.Response` when the task completes, or terminates if the task fails with an error. /// - 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. - /// - returns: The new request publisher. - func publisher(_ request: Request, callbackQueue: CallbackQueue? = nil) -> RequestPublisher { - return RequestPublisher(session: self, request: request, callbackQueue: callbackQueue) + /// - returns: A publisher that wraps a session task for the request. + func sessionTaskPublisher(for request: Request, callbackQueue: CallbackQueue? = nil) -> SessionTaskPublisher { + return SessionTaskPublisher(request: request, session: self, callbackQueue: callbackQueue) } } diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index a3e14d07..4406fca9 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -30,7 +30,7 @@ open class Session { return privateShared } - /// Calls `send(_:callbackQueue:handler:)` of `shared`. + /// 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. @@ -40,7 +40,7 @@ open class Session { return shared.send(request, callbackQueue: callbackQueue, handler: handler) } - /// Calls `cancelRequests(with:passingTest:)` of `sharedSession`. + /// Calls `cancelRequests(with:passingTest:)` of `Session.shared`. open class func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { shared.cancelRequests(with: requestType, passingTest: test) } diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift index 809af613..54795439 100644 --- a/Tests/APIKitTests/Combine/CombineTests.swift +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -30,7 +30,7 @@ final class CombineTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.publisher(request) + session.sessionTaskPublisher(for: request) .sink(receiveCompletion: { completion in switch completion { case .failure: @@ -52,7 +52,7 @@ final class CombineTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - session.publisher(request) + session.sessionTaskPublisher(for: request) .sink(receiveCompletion: { completion in if case .failure(let error) = completion, case .responseError(let responseError as NSError) = error { XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) @@ -73,7 +73,7 @@ final class CombineTests: XCTestCase { let expectation = self.expectation(description: "wait for response") let request = TestRequest() - let cancellable = session.publisher(request) + let cancellable = session.sessionTaskPublisher(for: request) .sink(receiveCompletion: { completion in if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { XCTAssertEqual(connectionError.code, 0) From 5cc72bcd527653468d36f1764e44c291b4cd1f9f Mon Sep 17 00:00:00 2001 From: Econa77 Date: Tue, 23 Feb 2021 17:09:55 +0900 Subject: [PATCH 07/31] Fix tests --- Tests/APIKitTests/Combine/CombineTests.swift | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift index 54795439..a253067b 100644 --- a/Tests/APIKitTests/Combine/CombineTests.swift +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -69,22 +69,51 @@ final class CombineTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } - func testCancel() { - let expectation = self.expectation(description: "wait for response") + func testBefore2020OSVersionsCancel() throws { + if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { + throw XCTSkip("Skip on After 2020 OS versions, as Combine cancellation no longer emits a value.") + } + + let cancelExpectation = self.expectation(description: "wait for cancel") + let completionExpectation = self.expectation(description: "wait for response") let request = TestRequest() let cancellable = session.sessionTaskPublisher(for: request) + .handleEvents(receiveCancel: { + cancelExpectation.fulfill() + }) .sink(receiveCompletion: { completion in if case .failure(let error) = completion, case .connectionError(let connectionError as NSError) = error { XCTAssertEqual(connectionError.code, 0) - expectation.fulfill() + completionExpectation.fulfill() } else { XCTFail() } }, receiveValue: { response in XCTFail() }) + cancellable.cancel() + + waitForExpectations(timeout: 1.0, handler: nil) + } + + func testAfter2020OSVersionsCancel() throws { + guard #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) else { + throw XCTSkip("Skip on Before 2020 OS versions.") + } + let expectation = self.expectation(description: "wait for cancel") + let request = TestRequest() + + let cancellable = session.sessionTaskPublisher(for: request) + .handleEvents(receiveCancel: { + expectation.fulfill() + }) + .sink(receiveCompletion: { completion in + XCTFail() + }, receiveValue: { response in + XCTFail() + }) cancellable.cancel() waitForExpectations(timeout: 1.0, handler: nil) From ad0743d7cf53100fea816c74561c52bfc5e46fe5 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Thu, 25 Feb 2021 00:31:56 +0900 Subject: [PATCH 08/31] Set version 5.2.0 --- APIKit.podspec | 2 +- Sources/APIKit/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/APIKit.podspec b/APIKit.podspec index d4d08ba5..df51df1f 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "APIKit" - s.version = "5.1.0" + s.version = "5.2.0" s.summary = "Type-safe networking abstraction layer that associates request type with response type." s.homepage = "https://github.com/ishkawa/APIKit" diff --git a/Sources/APIKit/Info.plist b/Sources/APIKit/Info.plist index 206d86b6..10e795d9 100644 --- a/Sources/APIKit/Info.plist +++ b/Sources/APIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.1.0 + 5.2.0 CFBundleSignature ???? CFBundleVersion From f095a9ccfe68bd083dc37937d885d85f94fbe22d Mon Sep 17 00:00:00 2001 From: Econa77 Date: Thu, 25 Feb 2021 22:42:04 +0900 Subject: [PATCH 09/31] Fix Demo.playground settings --- Demo.playground/contents.xcplayground | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground index b1148fc5..a8211e59 100644 --- a/Demo.playground/contents.xcplayground +++ b/Demo.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file From 1213937f9dcbdee9f69bea3fc94a7cf51099f56a Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 12:03:37 +0900 Subject: [PATCH 10/31] [CI] Fix DEVELOPER_DIR --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b339704b..c3da7132 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: macos-latest name: CocoaPods Lint env: - DEVELOPER_DIR: "/Applications/Xcode_12.3.app" + DEVELOPER_DIR: "/Applications/Xcode_12.3.app/Contents/Developer" steps: - uses: actions/checkout@v2 - run: pod lib lint --allow-warnings @@ -23,7 +23,7 @@ jobs: fail-fast: false name: Xcode ${{ matrix.xcode }} env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app" + DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" steps: - uses: actions/checkout@v2 with: @@ -46,7 +46,7 @@ jobs: fail-fast: false name: SwiftPM (Xcode ${{ matrix.xcode }}) env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app" + DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" steps: - uses: actions/checkout@v2 - name: build and test From a433a2e869255f5dd13b79c7ced75cfb65b48423 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 12:17:27 +0900 Subject: [PATCH 11/31] [CI] Change macOS image --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3da7132..696b1930 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: podspec: - runs-on: macos-latest + runs-on: macos-10.15 name: CocoaPods Lint env: DEVELOPER_DIR: "/Applications/Xcode_12.3.app/Contents/Developer" @@ -16,7 +16,7 @@ jobs: - run: pod lib lint --allow-warnings xcode: - runs-on: macos-latest + runs-on: macos-10.15 strategy: matrix: xcode: [10.3, 11.7, 12.3] @@ -39,7 +39,7 @@ jobs: if: ${{ success() }} swiftpm: - runs-on: macos-latest + runs-on: macos-10.15 strategy: matrix: xcode: [11.7, 12.3] From 3e4729d397d98520a87c681bfc85a0171b846e3e Mon Sep 17 00:00:00 2001 From: noppe Date: Sun, 17 Jan 2021 03:00:40 +0900 Subject: [PATCH 12/31] Change iOS deployment target for Xcode 12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the warning about iOS 8 no longer being supported in Xcode 12 while maintaining support for older versions of Xcode. Technically checking the compiler version isn’t an exact 1:1 for checking for Xcode version, but in practice should work in all scenarios. --- Package.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 9ce875e0..53cfe938 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,22 @@ // swift-tools-version:5.0 import PackageDescription +var platforms: [SupportedPlatform] { + #if compiler(<5.3) + return [ + .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2) + ] + #else + // Xcode 12 (which ships with Swift 5.3) drops support for iOS 8 + return [ + .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) + ] + #endif +} + let package = Package( name: "APIKit", - platforms: [ - .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2) - ], + platforms: platforms, products: [ .library(name: "APIKit", targets: ["APIKit"]), ], From c3562f37db2bc7cd3c4e6d12b354cb1d7e6ce9a2 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 11:48:36 +0900 Subject: [PATCH 13/31] Change iOS deployment target version --- Package.swift | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Package.swift b/Package.swift index 53cfe938..a9bf0445 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,11 @@ // swift-tools-version:5.0 import PackageDescription -var platforms: [SupportedPlatform] { - #if compiler(<5.3) - return [ - .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2) - ] - #else - // Xcode 12 (which ships with Swift 5.3) drops support for iOS 8 - return [ - .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) - ] - #endif -} - let package = Package( name: "APIKit", - platforms: platforms, + platforms: [ + .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) + ], products: [ .library(name: "APIKit", targets: ["APIKit"]), ], From 2292b02513c676826f479fd96054fdaab7c29e94 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 11:57:20 +0900 Subject: [PATCH 14/31] Also supported for CocoaPods and Carthage --- APIKit.podspec | 2 +- Configurations/Base.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/APIKit.podspec b/APIKit.podspec index df51df1f..908cfadd 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| "Yosuke Ishikawa" => "y@ishkawa.org" } - s.ios.deployment_target = "8.0" + s.ios.deployment_target = "9.0" s.osx.deployment_target = "10.10" if s.respond_to?(:watchos) s.watchos.deployment_target = "2.0" diff --git a/Configurations/Base.xcconfig b/Configurations/Base.xcconfig index cf59d8cd..3a036335 100644 --- a/Configurations/Base.xcconfig +++ b/Configurations/Base.xcconfig @@ -27,6 +27,6 @@ VERSIONING_SYSTEM = apple-generic CODE_SIGNING_REQUIRED = NO CODE_SIGN_IDENTITY = MACOSX_DEPLOYMENT_TARGET = 10.10 -IPHONEOS_DEPLOYMENT_TARGET = 8.0 +IPHONEOS_DEPLOYMENT_TARGET = 9.0 WATCHOS_DEPLOYMENT_TARGET = 2.0 TVOS_DEPLOYMENT_TARGET = 9.0 From 4e7f42d93afb787b0bc502171f9b5c12cf49d0ca Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 12:48:49 +0900 Subject: [PATCH 15/31] Set version 5.3.0 --- APIKit.podspec | 2 +- README.md | 2 +- Sources/APIKit/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/APIKit.podspec b/APIKit.podspec index 908cfadd..7afab61b 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "APIKit" - s.version = "5.2.0" + s.version = "5.3.0" s.summary = "Type-safe networking abstraction layer that associates request type with response type." s.homepage = "https://github.com/ishkawa/APIKit" diff --git a/README.md b/README.md index 9bc26fcb..e6b06ebb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Session.send(request) { result in ## Requirements - Swift 5.0 or later -- iOS 8.0 or later +- iOS 9.0 or later - Mac OS 10.10 or later - watchOS 2.0 or later - tvOS 9.0 or later diff --git a/Sources/APIKit/Info.plist b/Sources/APIKit/Info.plist index 10e795d9..2b29ada6 100644 --- a/Sources/APIKit/Info.plist +++ b/Sources/APIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.2.0 + 5.3.0 CFBundleSignature ???? CFBundleVersion From 551afaa735a2d6639237a2ff42c0b0da8ed0d30b Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 1 Jan 2022 13:03:32 +0900 Subject: [PATCH 16/31] Fix deprecated `class` keyword --- Sources/APIKit/SessionAdapter/SessionAdapter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index d123dae0..aa71ac5d 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -1,7 +1,7 @@ import Foundation /// `SessionTask` protocol represents a task for a request. -public protocol SessionTask: class { +public protocol SessionTask: AnyObject { func resume() func cancel() } From 3cfeeae3a5e8d8c1bd7fb57f88875498d4e39a4b Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 01:36:42 +0900 Subject: [PATCH 17/31] [CI] Update Xcode & macOS versions --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 696b1930..76f4852f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,28 +7,40 @@ on: jobs: podspec: - runs-on: macos-10.15 + runs-on: macos-12 name: CocoaPods Lint env: - DEVELOPER_DIR: "/Applications/Xcode_12.3.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" steps: - uses: actions/checkout@v2 - run: pod lib lint --allow-warnings xcode: - runs-on: macos-10.15 + name: ${{ matrix.xcode }} + runs-on: ${{ matrix.runsOn }} + env: + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" strategy: - matrix: - xcode: [10.3, 11.7, 12.3] fail-fast: false - name: Xcode ${{ matrix.xcode }} - env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" + matrix: + include: + - xcode: "Xcode_13.4.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" + - xcode: "Xcode_12.5.1" + runsOn: macOS-11 + name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" + - xcode: "Xcode_11.7" + runsOn: macOS-10.15 + name: "macOS 10.15, Xcode 11.7, Swift 5.2.4" + - xcode: "Xcode_10.3" + runsOn: macOS-10.15 + name: "macOS 10.15, Xcode 10.3, Swift 5.0.1" steps: - uses: actions/checkout@v2 with: fetch-depth: 2 - - name: test + - name: ${{ matrix.name }} run: | set -o pipefail xcodebuild build-for-testing test-without-building -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c @@ -39,17 +51,26 @@ jobs: if: ${{ success() }} swiftpm: - runs-on: macos-10.15 + name: SPM with ${{ matrix.xcode }} + runs-on: ${{ matrix.runsOn }} + env: + DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" strategy: - matrix: - xcode: [11.7, 12.3] fail-fast: false - name: SwiftPM (Xcode ${{ matrix.xcode }}) - env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" + matrix: + include: + - xcode: "Xcode_13.4.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" + - xcode: "Xcode_12.5.1" + runsOn: macOS-11 + name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" + - xcode: "Xcode_11.7" + runsOn: macOS-10.15 + name: "macOS 10.15, Xcode 11.7, Swift 5.2.4" steps: - uses: actions/checkout@v2 - - name: build and test + - name: ${{ matrix.name }} run: | swift build swift test From 646305d4038aa362b9ef63508439a9f198652224 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 03:06:33 +0900 Subject: [PATCH 18/31] Rerunning tests that were not run in SPM --- APIKit.xcodeproj/project.pbxproj | 26 ++++++++++++------- Package.swift | 5 ++-- .../MultipartFormDataParametersTests.swift | 12 +++++---- Tests/APIKitTests/{ => Resources}/test.json | 0 4 files changed, 27 insertions(+), 16 deletions(-) rename Tests/APIKitTests/{ => Resources}/test.json (100%) diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 8fc0f748..16ded583 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ 7F698E5A1D9D680C00F1561D /* URLSessionAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */; }; 7F698E5B1D9D680C00F1561D /* SessionCallbackQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */; }; 7F698E5C1D9D680C00F1561D /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4A1D9D680C00F1561D /* SessionTests.swift */; }; - 7F698E5D1D9D680C00F1561D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = 7F698E4B1D9D680C00F1561D /* test.json */; }; 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4D1D9D680C00F1561D /* TestRequest.swift */; }; 7F698E5F1D9D680C00F1561D /* TestSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */; }; 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */; }; @@ -48,6 +47,7 @@ 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */; }; 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; }; 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; }; + C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; 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 */; }; @@ -100,7 +100,6 @@ 7F698E481D9D680C00F1561D /* URLSessionAdapterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionAdapterTests.swift; sourceTree = ""; }; 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionCallbackQueueTests.swift; sourceTree = ""; }; 7F698E4A1D9D680C00F1561D /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; - 7F698E4B1D9D680C00F1561D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = ""; }; 7F698E4D1D9D680C00F1561D /* TestRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; 7F698E4E1D9D680C00F1561D /* TestSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionAdapter.swift; sourceTree = ""; }; 7F698E4F1D9D680C00F1561D /* TestSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionTask.swift; sourceTree = ""; }; @@ -129,6 +128,7 @@ 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLEncodedSerialization.swift; path = Sources/APIKit/Serializations/URLEncodedSerialization.swift; sourceTree = SOURCE_ROOT; }; 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStub.swift; sourceTree = ""; }; + C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; 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; }; @@ -201,8 +201,8 @@ ECA8314B1DE4E677004EB1B5 /* ProtobufBodyParameters.swift */, 7F7048D81D9D89FB003C99F6 /* AbstractInputStream.m */, ); - path = APIKit/BodyParameters; name = BodyParameters; + path = APIKit/BodyParameters; sourceTree = ""; }; 7F18BD161C9730ED003A31DF /* Serializations */ = { @@ -210,8 +210,8 @@ children = ( 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */, ); - path = APIKit/Serializations; name = Serializations; + path = APIKit/Serializations; sourceTree = ""; }; 7F45FCD31A94D02C006863BB = { @@ -245,6 +245,7 @@ 7F698E401D9D680C00F1561D /* DataParserType */, 7F698E461D9D680C00F1561D /* SessionAdapterType */, 7F698E4C1D9D680C00F1561D /* TestComponents */, + C5FF1DBF28A80FFD0059573D /* Resources */, 7F698E611D9D681500F1561D /* Supporting Files */, ); path = APIKitTests; @@ -297,7 +298,6 @@ isa = PBXGroup; children = ( 7F698E441D9D680C00F1561D /* Info.plist */, - 7F698E4B1D9D680C00F1561D /* test.json */, ); name = "Supporting Files"; sourceTree = ""; @@ -336,8 +336,8 @@ 7F7048D41D9D89F2003C99F6 /* SessionAdapter.swift */, 7F7048D51D9D89F2003C99F6 /* URLSessionAdapter.swift */, ); - path = APIKit/SessionAdapter; name = SessionAdapter; + path = APIKit/SessionAdapter; sourceTree = ""; }; 7FA19A3D1C9CBF2A005D25AE /* Error */ = { @@ -347,8 +347,8 @@ 7F7048EC1D9D8A12003C99F6 /* RequestError.swift */, 7F7048ED1D9D8A12003C99F6 /* ResponseError.swift */, ); - path = APIKit/Error; name = Error; + path = APIKit/Error; sourceTree = ""; }; 7FA19A441C9CC9A2005D25AE /* DataParser */ = { @@ -360,8 +360,16 @@ ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */, 7F7048E71D9D8A08003C99F6 /* StringDataParser.swift */, ); - path = APIKit/DataParser; name = DataParser; + path = APIKit/DataParser; + sourceTree = ""; + }; + C5FF1DBF28A80FFD0059573D /* Resources */ = { + isa = PBXGroup; + children = ( + C5FF1DC028A80FFD0059573D /* test.json */, + ); + path = Resources; sourceTree = ""; }; /* End PBXGroup section */ @@ -464,7 +472,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F698E5D1D9D680C00F1561D /* test.json in Resources */, + C5FF1DC128A80FFD0059573D /* test.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Package.swift b/Package.swift index a9bf0445..e55d20b6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.3 import PackageDescription let package = Package( @@ -18,7 +18,8 @@ let package = Package( ), .testTarget( name: "APIKitTests", - dependencies: ["APIKit"] + dependencies: ["APIKit"], + resources: [.process("Resources")] ), ], swiftLanguageVersions: [.v5] diff --git a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift index 4a0536dc..9cedcf04 100644 --- a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift @@ -71,11 +71,14 @@ class MultipartFormDataParametersTests: XCTestCase { // MARK: Values - // Skip test cases that uses files until SwiftPM supports resources. - #if !SWIFT_PACKAGE - func testFileValue() { - let fileURL = Bundle(for: type(of: self)).url(forResource: "test", withExtension: "json")! let part = try! MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") + func testFileValue() throws { + #if SWIFT_PACKAGE + let bundle = Bundle.module + #else + let bundle = Bundle(for: type(of: self)) + #endif + let fileURL = try XCTUnwrap(bundle.url(forResource: "test", withExtension: "json")) let parameters = MultipartFormDataBodyParameters(parts: [part]) do { @@ -103,7 +106,6 @@ class MultipartFormDataParametersTests: XCTestCase { XCTFail() } } - #endif func testStringValue() { let part = try! MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") diff --git a/Tests/APIKitTests/test.json b/Tests/APIKitTests/Resources/test.json similarity index 100% rename from Tests/APIKitTests/test.json rename to Tests/APIKitTests/Resources/test.json From 872402da2035058d568ac8a2d9f169eab8b4e826 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 03:09:18 +0900 Subject: [PATCH 19/31] Improve tests that throw errors --- .../JSONBodyParametersTests.swift | 47 ++--- .../MultipartFormDataParametersTests.swift | 175 ++++++++---------- .../ProtobufBodyParametersTests.swift | 22 +-- .../URLEncodedSerializationTests.swift | 23 +-- Tests/APIKitTests/Combine/CombineTests.swift | 4 +- .../FormURLEncodedDataParserTests.swift | 21 +-- .../DataParserType/JSONDataParserTests.swift | 16 +- .../ProtobufDataParserTests.swift | 19 +- .../StringDataParserTests.swift | 15 +- Tests/APIKitTests/RequestTests.swift | 41 ++-- .../SessionCallbackQueueTests.swift | 10 +- Tests/APIKitTests/SessionTests.swift | 4 +- 12 files changed, 158 insertions(+), 239 deletions(-) diff --git a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift index 512eb749..0bc9b535 100644 --- a/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/JSONBodyParametersTests.swift @@ -3,58 +3,47 @@ import XCTest import APIKit class JSONBodyParametersTests: XCTestCase { - func testJSONSuccess() { + func testJSONSuccess() throws { let object = ["foo": 1, "bar": 2, "baz": 3] let parameters = JSONBodyParameters(JSONObject: object) XCTAssertEqual(parameters.contentType, "application/json") - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let dictionary = try JSONSerialization.jsonObject(with: data, options: []) - XCTAssertEqual((dictionary as? [String: Int])?["foo"], 1) - XCTAssertEqual((dictionary as? [String: Int])?["bar"], 2) - XCTAssertEqual((dictionary as? [String: Int])?["baz"], 3) - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let dictionary = try JSONSerialization.jsonObject(with: data, options: []) + XCTAssertEqual((dictionary as? [String: Int])?["foo"], 1) + XCTAssertEqual((dictionary as? [String: Int])?["bar"], 2) + XCTAssertEqual((dictionary as? [String: Int])?["baz"], 3) } func testJSONFailure() { let object = NSObject() let parameters = JSONBodyParameters(JSONObject: object) - do { - try _ = parameters.buildEntity() - XCTFail() - } catch { + XCTAssertThrowsError(try parameters.buildEntity()) { error in let nserror = error as NSError XCTAssertEqual(nserror.domain, NSCocoaErrorDomain) XCTAssertEqual(nserror.code, 3840) } } - func testDictionaryLiteral() { + func testDictionaryLiteral() throws { let object = ["foo": 1, "bar": 2, "baz": 3] let parameters1: JSONBodyParameters = .init(JSONObject: object) let parameters2: JSONBodyParameters = ["foo": 1, "bar": 2, "baz": 3] - do { - guard case .data(let data1) = try parameters1.buildEntity(), - case .data(let data2) = try parameters2.buildEntity() else { - XCTFail() - return - } - let dictionary1 = try JSONSerialization.jsonObject(with: data1, options: []) - let dictionary2 = try JSONSerialization.jsonObject(with: data2, options: []) - XCTAssertEqual((dictionary1 as? [String: Int])?["foo"], (dictionary2 as? [String: Int])?["foo"]) - XCTAssertEqual((dictionary1 as? [String: Int])?["bar"], (dictionary2 as? [String: Int])?["bar"]) - XCTAssertEqual((dictionary1 as? [String: Int])?["baz"], (dictionary2 as? [String: Int])?["baz"]) - } catch { + + guard case .data(let data1) = try parameters1.buildEntity(), + case .data(let data2) = try parameters2.buildEntity() else { XCTFail() + return } + let dictionary1 = try JSONSerialization.jsonObject(with: data1, options: []) + let dictionary2 = try JSONSerialization.jsonObject(with: data2, options: []) + XCTAssertEqual((dictionary1 as? [String: Int])?["foo"], (dictionary2 as? [String: Int])?["foo"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["bar"], (dictionary2 as? [String: Int])?["bar"]) + XCTAssertEqual((dictionary1 as? [String: Int])?["baz"], (dictionary2 as? [String: Int])?["baz"]) } } diff --git a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift index 9cedcf04..faf68949 100644 --- a/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/MultipartFormDataParametersTests.swift @@ -4,74 +4,65 @@ import XCTest class MultipartFormDataParametersTests: XCTestCase { // MARK: Entity - func testDataEntitySuccess() { - let value1 = "1".data(using: .utf8)! - let value2 = "2".data(using: .utf8)! + func testDataEntitySuccess() throws { + let value1 = try XCTUnwrap("1".data(using: .utf8)) + let value2 = try XCTUnwrap("2".data(using: .utf8)) let parameters = MultipartFormDataBodyParameters(parts: [ MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), ]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") } - func testInputStreamEntitySuccess() { - let value1 = "1".data(using: .utf8)! - let value2 = "2".data(using: .utf8)! + func testInputStreamEntitySuccess() throws { + let value1 = try XCTUnwrap("1".data(using: .utf8)) + let value2 = try XCTUnwrap("2".data(using: .utf8)) let parameters = MultipartFormDataBodyParameters(parts: [ MultipartFormDataBodyParameters.Part(data: value1, name: "foo"), MultipartFormDataBodyParameters.Part(data: value2, name: "bar"), ], entityType: .inputStream) - do { - guard case .inputStream(let inputStream) = try parameters.buildEntity() else { - XCTFail() - return - } - - let data = try Data(inputStream: inputStream) - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .inputStream(let inputStream) = try parameters.buildEntity() else { XCTFail() + return } + let data = try Data(inputStream: inputStream) + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"foo\"\(returnCode)\(returnCode)1\(returnCode)--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"bar\"\(returnCode)\(returnCode)2\(returnCode)--\(boundary)--\(returnCode)") } // MARK: Values - let part = try! MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") func testFileValue() throws { #if SWIFT_PACKAGE let bundle = Bundle.module @@ -79,82 +70,64 @@ class MultipartFormDataParametersTests: XCTestCase { let bundle = Bundle(for: type(of: self)) #endif let fileURL = try XCTUnwrap(bundle.url(forResource: "test", withExtension: "json")) + let part = try MultipartFormDataBodyParameters.Part(fileURL: fileURL, name: "test") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let testData = try! Data(contentsOf: fileURL) - let testString = String(data: testData, encoding: .utf8)! - - let encodedData = String(data: data, encoding:.utf8)! - let returnCode = "\r\n" - - let pattern = "^multipart/form-data; boundary=([\\w.]+)$" - let regexp = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: parameters.contentType.count) - let match = regexp.matches(in: parameters.contentType, options: [], range: range) - XCTAssertTrue(match.count > 0) - - let boundary = (parameters.contentType as NSString).substring(with: match.first!.range(at: 1)) - XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") - XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"test\"; filename=\"test.json\"\r\nContent-Type: application/json\(returnCode)\(returnCode)\(testString)\(returnCode)--\(boundary)--\(returnCode)") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let testData = try Data(contentsOf: fileURL) + let testString = try XCTUnwrap(String(data: testData, encoding: .utf8)) + + let encodedData = try XCTUnwrap(String(data: data, encoding:.utf8)) + let returnCode = "\r\n" + + let pattern = "^multipart/form-data; boundary=([\\w.]+)$" + let regexp = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: parameters.contentType.count) + let match = regexp.matches(in: parameters.contentType, options: [], range: range) + XCTAssertTrue(match.count > 0) + + let firstRange = try XCTUnwrap(match.first?.range(at: 1)) + let boundary = (parameters.contentType as NSString).substring(with: firstRange) + XCTAssertEqual(parameters.contentType, "multipart/form-data; boundary=\(boundary)") + XCTAssertEqual(encodedData, "--\(boundary)\(returnCode)Content-Disposition: form-data; name=\"test\"; filename=\"test.json\"\r\nContent-Type: application/json\(returnCode)\(returnCode)\(testString)\(returnCode)--\(boundary)--\(returnCode)") } - func testStringValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") + func testStringValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: "abcdef", name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nabcdef\r\n--\(parameters.boundary)--\r\n") } - func testIntValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: 123, name: "foo") + func testIntValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: 123, name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n123\r\n--\(parameters.boundary)--\r\n") } - func testDoubleValue() { - let part = try! MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") + func testDoubleValue() throws { + let part = try MultipartFormDataBodyParameters.Part(value: 3.14, name: "foo") let parameters = MultipartFormDataBodyParameters(parts: [part]) - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let string = String(data: data, encoding:.utf8)! - XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } + let string = String(data: data, encoding:.utf8) + XCTAssertEqual(string, "--\(parameters.boundary)\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--\(parameters.boundary)--\r\n") } } diff --git a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift index e58102d3..3f5da8b4 100644 --- a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift @@ -3,26 +3,18 @@ import XCTest import APIKit class ProtobufBodyParametersTests: XCTestCase { - func testProtobufSuccess() { - // Temporarily gated due to https://openradar.appspot.com/49262697 - if #available(macOS 10.11, *) { + func testProtobufSuccess() throws { let object = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) let parameters = ProtobufBodyParameters(protobufObject: object) XCTAssertEqual(parameters.contentType, "application/protobuf") - do { - guard case .data(let data) = try parameters.buildEntity() else { - XCTFail() - return - } - - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { + guard case .data(let data) = try parameters.buildEntity() else { XCTFail() + return } - } + let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Int] + XCTAssertEqual(dictionary?["foo"], 1) + XCTAssertEqual(dictionary?["bar"], 2) + XCTAssertEqual(dictionary?["baz"], 3) } } diff --git a/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift b/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift index 89702e4d..64d33cf4 100644 --- a/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift +++ b/Tests/APIKitTests/BodyParametersType/URLEncodedSerializationTests.swift @@ -4,21 +4,18 @@ import APIKit class URLEncodedSerializationTests: XCTestCase { // MARK: NSData -> Any - func testObjectFromData() { - let data = "key1=value1&key2=value2".data(using: .utf8)! + func testObjectFromData() throws { + let data = try XCTUnwrap("key1=value1&key2=value2".data(using: .utf8)) let object = try? URLEncodedSerialization.object(from: data, encoding: .utf8) XCTAssertEqual(object?["key1"], "value1") XCTAssertEqual(object?["key2"], "value2") } - func testInvalidFormatString() { + func testInvalidFormatString() throws { let string = "key==value&" - do { - let data = string.data(using: .utf8)! - try _ = URLEncodedSerialization.object(from: data, encoding: .utf8) - XCTFail() - } catch { + let data = try XCTUnwrap(string.data(using: .utf8)) + XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .invalidFormatString(let invalidString) = error else { XCTFail() @@ -33,10 +30,7 @@ class URLEncodedSerializationTests: XCTestCase { var bytes = [UInt8]([0xed, 0xa0, 0x80]) // U+D800 (high surrogate) let data = Data(bytes: &bytes, count: bytes.count) - do { - try _ = URLEncodedSerialization.object(from: data, encoding: .utf8) - XCTFail() - } catch { + XCTAssertThrowsError(try URLEncodedSerialization.object(from: data, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .cannotGetStringFromData(let invalidData, let encoding) = error else { XCTFail() @@ -59,10 +53,7 @@ class URLEncodedSerializationTests: XCTestCase { func testNonDictionaryObject() { let dictionaries = [["hey": "yo"]] as Any - do { - try _ = URLEncodedSerialization.data(from: dictionaries, encoding: .utf8) - XCTFail() - } catch { + XCTAssertThrowsError(try URLEncodedSerialization.data(from: dictionaries, encoding: .utf8)) { error in guard let error = error as? URLEncodedSerialization.Error, case .cannotCastObjectToDictionary(let object) = error else { XCTFail() diff --git a/Tests/APIKitTests/Combine/CombineTests.swift b/Tests/APIKitTests/Combine/CombineTests.swift index a253067b..32c548b9 100644 --- a/Tests/APIKitTests/Combine/CombineTests.swift +++ b/Tests/APIKitTests/Combine/CombineTests.swift @@ -23,9 +23,9 @@ final class CombineTests: XCTestCase { cancellables = [] } - func testSuccess() { + func testSuccess() throws { let dictionary = ["key": "value"] - adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) let expectation = self.expectation(description: "wait for response") let request = TestRequest() diff --git a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift index 8bf66a77..cb2f5357 100644 --- a/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/FormURLEncodedDataParserTests.swift @@ -8,20 +8,16 @@ class FormURLEncodedDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/x-www-form-urlencoded") } - func testURLSuccess() { + func testURLSuccess() throws { let string = "foo=1&bar=2&baz=3" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = FormURLEncodedDataParser(encoding: .utf8) - do { - 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") - } catch { - XCTFail() - } + 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") } func testInvalidString() { @@ -29,10 +25,7 @@ class FormURLEncodedDataParserTests: XCTestCase { let data = Data(bytes: &bytes, count: bytes.count) let parser = FormURLEncodedDataParser(encoding: .utf8) - do { - try _ = parser.parse(data: data) - XCTFail() - } catch { + XCTAssertThrowsError(try parser.parse(data: data)) { error in guard let error = error as? FormURLEncodedDataParser.Error, case .cannotGetStringFromData(let invalidData) = error else { XCTFail() diff --git a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift index e7faaca4..3cf81427 100644 --- a/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/JSONDataParserTests.swift @@ -8,19 +8,15 @@ class JSONDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/json") } - func testJSONSuccess() { + func testJSONSuccess() throws { let string = "{\"foo\": 1, \"bar\": 2, \"baz\": 3}" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = JSONDataParser(readingOptions: []) - do { - let object = try parser.parse(data: data) - let dictionary = object as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { - XCTFail() - } + let object = try parser.parse(data: data) + let dictionary = object as? [String: Int] + XCTAssertEqual(dictionary?["foo"], 1) + XCTAssertEqual(dictionary?["bar"], 2) + XCTAssertEqual(dictionary?["baz"], 3) } } diff --git a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift index 968b31e0..6752bee9 100644 --- a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift @@ -8,21 +8,14 @@ class ProtobufDataParserTests: XCTestCase { XCTAssertEqual(parser.contentType, "application/protobuf") } - func testProtobufSuccess() { - // Temporarily gated due to https://openradar.appspot.com/49262697 - if #available(macOS 10.11, *) { + func testProtobufSuccess() throws { let data = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) let parser = ProtobufDataParser() - do { - let object = try parser.parse(data: data) as! Data - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: object) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) - } catch { - XCTFail() - } - } + let object = try XCTUnwrap(try parser.parse(data: data) as? Data) + let dictionary = NSKeyedUnarchiver.unarchiveObject(with: object) as? [String: Int] + XCTAssertEqual(dictionary?["foo"], 1) + XCTAssertEqual(dictionary?["bar"], 2) + XCTAssertEqual(dictionary?["baz"], 3) } } diff --git a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift index 0dc0255a..d932501a 100644 --- a/Tests/APIKitTests/DataParserType/StringDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/StringDataParserTests.swift @@ -8,17 +8,13 @@ class StringDataParserTests: XCTestCase { XCTAssertNil(parser.contentType) } - func testParseData() { + func testParseData() throws { let string = "abcdef" let data = string.data(using: .utf8, allowLossyConversion: false)! let parser = StringDataParser(encoding: .utf8) - do { - let object = try parser.parse(data: data) - XCTAssertEqual(object as? String, string) - } catch { - XCTFail() - } + let object = try parser.parse(data: data) + XCTAssertEqual(object as? String, string) } func testInvalidString() { @@ -26,10 +22,7 @@ class StringDataParserTests: XCTestCase { let data = Data(bytes: &bytes, count: bytes.count) let parser = StringDataParser(encoding: .utf8) - do { - try _ = parser.parse(data: data) - XCTFail() - } catch { + XCTAssertThrowsError(try parser.parse(data: data)) { error in guard let error = error as? StringDataParser.Error, case .invalidData(let invalidData) = error else { XCTFail() diff --git a/Tests/APIKitTests/RequestTests.swift b/Tests/APIKitTests/RequestTests.swift index bd10942b..88bfa72c 100644 --- a/Tests/APIKitTests/RequestTests.swift +++ b/Tests/APIKitTests/RequestTests.swift @@ -2,33 +2,33 @@ import XCTest import APIKit class RequestTests: XCTestCase { - func testJapanesesQueryParameters() { + func testJapanesesQueryParameters() throws { let request = TestRequest(parameters: ["q": "こんにちは"]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") } - func testSymbolQueryParameters() { + func testSymbolQueryParameters() throws { let request = TestRequest(parameters: ["q": "!\"#$%&'()0=~|`{}*+<>?/_"]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "q=%21%22%23%24%25%26%27%28%290%3D~%7C%60%7B%7D%2A%2B%3C%3E?/_") } - func testNullQueryParameters() { + func testNullQueryParameters() throws { let request = TestRequest(parameters: ["null": NSNull()]) - let urlRequest = try? request.buildURLRequest() - XCTAssertEqual(urlRequest?.url?.query, "null") + let urlRequest = try request.buildURLRequest() + XCTAssertEqual(urlRequest.url?.query, "null") } - func testheaderFields() { + func testheaderFields() throws { let request = TestRequest(headerFields: ["Foo": "f", "Accept": "a", "Content-Type": "c"]) - let urlReqeust = try? request.buildURLRequest() - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Foo"), "f") - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Accept"), "a") - XCTAssertEqual(urlReqeust?.value(forHTTPHeaderField: "Content-Type"), "c") + let urlReqeust = try request.buildURLRequest() + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Foo"), "f") + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Accept"), "a") + XCTAssertEqual(urlReqeust.value(forHTTPHeaderField: "Content-Type"), "c") } - func testPOSTJSONRequest() { + func testPOSTJSONRequest() throws { let parameters: [Any] = [ ["id": "1"], ["id": "2"], @@ -38,10 +38,9 @@ class RequestTests: XCTestCase { let request = TestRequest(method: .post, parameters: parameters) XCTAssert((request.parameters as? [Any])?.count == 3) - let urlRequest = try? request.buildURLRequest() - XCTAssertNotNil(urlRequest?.httpBody) + let urlRequest = try request.buildURLRequest() - let json = urlRequest?.httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [AnyObject] + let json = urlRequest.httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [AnyObject] XCTAssertEqual(json?.count, 3) XCTAssertEqual((json?[0] as? [String: String])?["id"], "1") XCTAssertEqual((json?[1] as? [String: String])?["id"], "2") @@ -409,12 +408,12 @@ class RequestTests: XCTestCase { ) } - func testInterceptURLRequest() { - let url = URL(string: "https://example.com/customize")! + func testInterceptURLRequest() throws { + let url = try XCTUnwrap(URL(string: "https://example.com/customize")) let request = TestRequest() { _ in return URLRequest(url: url) } - XCTAssertEqual((try? request.buildURLRequest())?.url, url) + XCTAssertEqual(try request.buildURLRequest().url, url) } } diff --git a/Tests/APIKitTests/SessionCallbackQueueTests.swift b/Tests/APIKitTests/SessionCallbackQueueTests.swift index 66f2f494..b9d961f9 100644 --- a/Tests/APIKitTests/SessionCallbackQueueTests.swift +++ b/Tests/APIKitTests/SessionCallbackQueueTests.swift @@ -6,11 +6,11 @@ class SessionCallbackQueueTests: XCTestCase { var adapter: TestSessionAdapter! var session: Session! - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() adapter = TestSessionAdapter() - adapter.data = try! JSONSerialization.data(withJSONObject: ["key": "value"], options: []) + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: ["key": "value"], options: [])) session = Session(adapter: adapter, callbackQueue: .main) } @@ -33,7 +33,7 @@ class SessionCallbackQueueTests: XCTestCase { session.send(request, callbackQueue: .sessionQueue) { result in // This depends on implementation of TestSessionAdapter - XCTAssert(Thread.isMainThread) + XCTAssertTrue(Thread.isMainThread) expectation.fulfill() } @@ -60,7 +60,7 @@ class SessionCallbackQueueTests: XCTestCase { session.send(request, callbackQueue: .dispatchQueue(dispatchQueue)) { result in // There is no way to test current dispatch queue. - XCTAssert(!Thread.isMainThread) + XCTAssertFalse(Thread.isMainThread) expectation.fulfill() } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index d65a235e..fa9e4fb9 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -13,9 +13,9 @@ class SessionTests: XCTestCase { session = Session(adapter: adapter) } - func testSuccess() { + func testSuccess() throws { let dictionary = ["key": "value"] - adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) let expectation = self.expectation(description: "wait for response") let request = TestRequest() From cf72a213a60ac0dce0401dac4499aeb58fe51482 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 03:21:48 +0900 Subject: [PATCH 20/31] Drop support for swift 5.3 and below --- .github/workflows/ci.yml | 11 ++++------- .swift-version | 1 - 2 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 .swift-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76f4852f..57a55945 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,12 +30,9 @@ jobs: - xcode: "Xcode_12.5.1" runsOn: macOS-11 name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" - - xcode: "Xcode_11.7" + - xcode: "Xcode_12" runsOn: macOS-10.15 - name: "macOS 10.15, Xcode 11.7, Swift 5.2.4" - - xcode: "Xcode_10.3" - runsOn: macOS-10.15 - name: "macOS 10.15, Xcode 10.3, Swift 5.0.1" + name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" steps: - uses: actions/checkout@v2 with: @@ -65,9 +62,9 @@ jobs: - xcode: "Xcode_12.5.1" runsOn: macOS-11 name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" - - xcode: "Xcode_11.7" + - xcode: "Xcode_12" runsOn: macOS-10.15 - name: "macOS 10.15, Xcode 11.7, Swift 5.2.4" + name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" steps: - uses: actions/checkout@v2 - name: ${{ matrix.name }} diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 819e07a2..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -5.0 From 6235703b893ace8d45c14ba34058967aed30234b Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 03:33:54 +0900 Subject: [PATCH 21/31] Change commands depending on execution environment --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57a55945..6f8a1880 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,15 +59,16 @@ jobs: - xcode: "Xcode_13.4.1" runsOn: macOS-12 name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" + action: swift test -c debug - xcode: "Xcode_12.5.1" runsOn: macOS-11 name: "macOS 11, Xcode 12.5.1, Swift 5.4.2" + action: swift test -c debug - xcode: "Xcode_12" runsOn: macOS-10.15 name: "macOS 10.15, Xcode 12.0.1, Swift 5.3" + action: swift build -c debug steps: - uses: actions/checkout@v2 - name: ${{ matrix.name }} - run: | - swift build - swift test + run: ${{ matrix.action }} From b4903640d3081d728926d0f071d45c99a017cc3e Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 03:36:39 +0900 Subject: [PATCH 22/31] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e6b06ebb..45c1bed7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Session.send(request) { result in ## Requirements -- Swift 5.0 or later +- Swift 5.3 or later - iOS 9.0 or later - Mac OS 10.10 or later - watchOS 2.0 or later @@ -40,6 +40,8 @@ If you use Swift 2.2 or 2.3, try [APIKit 2.0.5](https://github.com/ishkawa/APIKi If you use Swift 4.2 or before, try [APIKit 4.1.0](https://github.com/ishkawa/APIKit/tree/4.1.0). +If you use Swift 5.2 or before, try [APIKit 5.3.0](https://github.com/ishkawa/APIKit/tree/5.3.0). + ## Installation #### [Carthage](https://github.com/Carthage/Carthage) From a001b653819e49bdd4629ea7159efdb4d06497ad Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 14 Aug 2022 04:05:09 +0900 Subject: [PATCH 23/31] Added default implementation of `response(from:urlResponse:)` when Response is Void --- Sources/APIKit/Request.swift | 6 ++++++ Tests/APIKitTests/SessionTests.swift | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/APIKit/Request.swift b/Sources/APIKit/Request.swift index efe68c97..ea6c5dae 100644 --- a/Sources/APIKit/Request.swift +++ b/Sources/APIKit/Request.swift @@ -145,3 +145,9 @@ public extension Request { return try response(from: passedObject, urlResponse: urlResponse) } } + +public extension Request where Response == Void { + func response(from object: Any, urlResponse: HTTPURLResponse) throws { + return + } +} diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index d65a235e..28777736 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -188,10 +188,6 @@ class SessionTests: XCTestCase { var path: String { return "/" } - - func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { - return () - } } func testCancelOtherRequest() { From 26a94b0721c25ae5871bbd733b8740735dde4bf7 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Mon, 7 Feb 2022 02:28:46 +0900 Subject: [PATCH 24/31] Try Swift Concurrency --- APIKit.xcodeproj/project.pbxproj | 13 ++++++ Sources/APIKit/Concurrency/Concurrency.swift | 45 ++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 Sources/APIKit/Concurrency/Concurrency.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index 16ded583..c3c90aa0 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 7F7048F11D9D8A12003C99F6 /* SessionTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048EE1D9D8A12003C99F6 /* SessionTaskError.swift */; }; 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; }; 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; }; + C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; }; ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; }; @@ -128,6 +129,7 @@ 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLEncodedSerialization.swift; path = Sources/APIKit/Serializations/URLEncodedSerialization.swift; sourceTree = SOURCE_ROOT; }; 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 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 = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; 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 = ""; }; @@ -310,6 +312,7 @@ 7F7048CA1D9D89BE003C99F6 /* Request.swift */, 7F7048CB1D9D89BE003C99F6 /* Session.swift */, 7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */, + C5725F4928D8C36500810D7C /* Concurrency */, 0969AE0D259DEC3C00C498AF /* Combine */, 7F85FB8B1C9D317300CEE132 /* SessionAdapter */, 7F18BD0D1C972C38003A31DF /* BodyParameters */, @@ -364,6 +367,15 @@ path = APIKit/DataParser; sourceTree = ""; }; + C5725F4928D8C36500810D7C /* Concurrency */ = { + isa = PBXGroup; + children = ( + C5725F4A28D8C36500810D7C /* Concurrency.swift */, + ); + name = Concurrency; + path = APIKit/Concurrency; + sourceTree = ""; + }; C5FF1DBF28A80FFD0059573D /* Resources */ = { isa = PBXGroup; children = ( @@ -495,6 +507,7 @@ 7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */, 7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */, 7F7048E21D9D89FB003C99F6 /* JSONBodyParameters.swift in Sources */, + C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */, 7F7048D61D9D89F2003C99F6 /* SessionAdapter.swift in Sources */, 7F7048EF1D9D8A12003C99F6 /* RequestError.swift in Sources */, 7F7048E91D9D8A08003C99F6 /* FormURLEncodedDataParser.swift in Sources */, diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift new file mode 100644 index 00000000..ca9e5233 --- /dev/null +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -0,0 +1,45 @@ +#if compiler(>=5.5.2) && canImport(_Concurrency) + +import Foundation + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private actor SessionTaskActor { + private weak var sessionTask: SessionTask? + + func send(_ sessionTask: SessionTask?) { + self.sessionTask = sessionTask + } + + func cancel() { + sessionTask?.cancel() + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Session { + static func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { + return try await shared.response(for: request, callbackQueue: callbackQueue) + } + + func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { + let sessionTaskActor = SessionTaskActor() + return try await withTaskCancellationHandler(operation: { + return try await withUnsafeThrowingContinuation { continuation in + Task { + await sessionTaskActor.send(send(request, callbackQueue: callbackQueue) { result in + switch result { + case .success(let response): + continuation.resume(returning: response) + case .failure(let error): + continuation.resume(throwing: error) + } + }) + } + } + }, onCancel: { + Task { await sessionTaskActor.cancel() } + }) + } +} + +#endif From eebbc027901b450cb4522fd403f669a0b02f78d8 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Tue, 20 Sep 2022 02:00:10 +0900 Subject: [PATCH 25/31] Rename SessionTask Actor to SessionTaskCancellationHandler --- Sources/APIKit/Concurrency/Concurrency.swift | 37 +++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index ca9e5233..03866e95 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -2,19 +2,6 @@ import Foundation -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -private actor SessionTaskActor { - private weak var sessionTask: SessionTask? - - func send(_ sessionTask: SessionTask?) { - self.sessionTask = sessionTask - } - - func cancel() { - sessionTask?.cancel() - } -} - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Session { static func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { @@ -22,24 +9,40 @@ public extension Session { } func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { - let sessionTaskActor = SessionTaskActor() + let cancellationHandler = SessionTaskCancellationHandler() return try await withTaskCancellationHandler(operation: { return try await withUnsafeThrowingContinuation { continuation in Task { - await sessionTaskActor.send(send(request, callbackQueue: callbackQueue) { result in + let sessionTask = send(request, callbackQueue: callbackQueue) { result in switch result { case .success(let response): continuation.resume(returning: response) case .failure(let error): continuation.resume(throwing: error) } - }) + } + await cancellationHandler.register(with: sessionTask) } } }, onCancel: { - Task { await sessionTaskActor.cancel() } + Task { await cancellationHandler.cancel() } }) } } +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private actor SessionTaskCancellationHandler { + private var sessionTask: SessionTask? + + func register(with task: SessionTask?) { + guard sessionTask == nil else { return } + sessionTask = task + } + + func cancel() { + sessionTask?.cancel() + sessionTask = nil + } +} + #endif From c09b881350d7bfe9642e584ad5b611fc6b6a765c Mon Sep 17 00:00:00 2001 From: Econa77 Date: Tue, 20 Sep 2022 03:29:26 +0900 Subject: [PATCH 26/31] Add concurrency tests --- APIKit.xcodeproj/project.pbxproj | 12 ++++ Sources/APIKit/Combine/Combine.swift | 1 + Sources/APIKit/Concurrency/Concurrency.swift | 23 +++++-- Sources/APIKit/Error/SessionTaskError.swift | 3 + .../Concurrency/ConcurrencyTests.swift | 68 +++++++++++++++++++ 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 Tests/APIKitTests/Concurrency/ConcurrencyTests.swift diff --git a/APIKit.xcodeproj/project.pbxproj b/APIKit.xcodeproj/project.pbxproj index c3c90aa0..e460ab57 100644 --- a/APIKit.xcodeproj/project.pbxproj +++ b/APIKit.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; }; 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 */; }; C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; }; ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; }; ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; }; @@ -130,6 +131,7 @@ 7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 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 = ""; }; C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; 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 = ""; }; @@ -242,6 +244,7 @@ 7F698E451D9D680C00F1561D /* RequestTests.swift */, 7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */, 7F698E4A1D9D680C00F1561D /* SessionTests.swift */, + C5B144D628D8D7D000E30ECD /* Concurrency */, 0973EE33259E2DD000879BA2 /* Combine */, 7F698E3B1D9D680C00F1561D /* BodyParametersType */, 7F698E401D9D680C00F1561D /* DataParserType */, @@ -376,6 +379,14 @@ path = APIKit/Concurrency; sourceTree = ""; }; + C5B144D628D8D7D000E30ECD /* Concurrency */ = { + isa = PBXGroup; + children = ( + C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */, + ); + path = Concurrency; + sourceTree = ""; + }; C5FF1DBF28A80FFD0059573D /* Resources */ = { isa = PBXGroup; children = ( @@ -534,6 +545,7 @@ 7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */, ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */, 7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */, + C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */, 7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */, 0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */, 7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */, diff --git a/Sources/APIKit/Combine/Combine.swift b/Sources/APIKit/Combine/Combine.swift index 0b4db3f4..6e3223c0 100644 --- a/Sources/APIKit/Combine/Combine.swift +++ b/Sources/APIKit/Combine/Combine.swift @@ -68,6 +68,7 @@ public struct SessionTaskPublisher: Publisher { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Session { /// Calls `sessionTaskPublisher(for:callbackQueue:)` 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. /// - returns: A publisher that wraps a session task for the request. diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index 03866e95..2555fc92 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -4,22 +4,31 @@ import Foundation @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Session { + /// Calls `response(for:callbackQueue:)` 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. + /// - returns: `Request.Response` static func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { return try await shared.response(for: request, callbackQueue: callbackQueue) } + /// Convenience method to load `Request.Response` using an `Request`, creates and resumes an `SessionTask` internally. + /// + /// - 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. + /// - returns: `Request.Response` func response(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response { let cancellationHandler = SessionTaskCancellationHandler() return try await withTaskCancellationHandler(operation: { - return try await withUnsafeThrowingContinuation { continuation in + return try await withCheckedThrowingContinuation { continuation in + guard !Task.isCancelled else { + continuation.resume(throwing: SessionTaskError.taskAlreadyCancelledError) + return + } Task { let sessionTask = send(request, callbackQueue: callbackQueue) { result in - switch result { - case .success(let response): - continuation.resume(returning: response) - case .failure(let error): - continuation.resume(throwing: error) - } + continuation.resume(with: result) } await cancellationHandler.register(with: sessionTask) } diff --git a/Sources/APIKit/Error/SessionTaskError.swift b/Sources/APIKit/Error/SessionTaskError.swift index 864d9287..c019714d 100644 --- a/Sources/APIKit/Error/SessionTaskError.swift +++ b/Sources/APIKit/Error/SessionTaskError.swift @@ -10,4 +10,7 @@ public enum SessionTaskError: Error { /// Error while creating `Request.Response` from `(Data, URLResponse)`. case responseError(Error) + + /// Error when the `Task` in Concurrency was cancelled before execution. + case taskAlreadyCancelledError } diff --git a/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift new file mode 100644 index 00000000..39b51c2a --- /dev/null +++ b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift @@ -0,0 +1,68 @@ +#if compiler(>=5.6.0) && canImport(_Concurrency) + +import XCTest +import APIKit + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +final class ConcurrencyTests: XCTestCase { + var adapter: TestSessionAdapter! + var session: Session! + + override func setUp() { + super.setUp() + adapter = TestSessionAdapter() + session = Session(adapter: adapter) + } + + func testSuccess() async throws { + let dictionary = ["key": "value"] + adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: [])) + + let request = TestRequest() + let value = try await session.response(for: request) + XCTAssertEqual((value as? [String: String])?["key"], "value") + } + + func testParseDataError() async throws { + adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false) + + let request = TestRequest() + do { + _ = try await session.response(for: request) + XCTFail() + } catch { + let sessionError = try XCTUnwrap(error as? SessionTaskError) + if case .responseError(let responseError as NSError) = sessionError { + XCTAssertEqual(responseError.domain, NSCocoaErrorDomain) + XCTAssertEqual(responseError.code, 3840) + } else { + XCTFail() + } + } + } + + func testCancel() async throws { + let request = TestRequest() + + let task = Task { + do { + _ = try await session.response(for: request) + XCTFail() + } catch { + let sessionError = try XCTUnwrap(error as? SessionTaskError) + switch sessionError { + case .taskAlreadyCancelledError: + XCTAssertTrue(Task.isCancelled) + default: + XCTFail() + } + } + } + task.cancel() + _ = try await task.value + + XCTAssertTrue(task.isCancelled) + } +} + +#endif From 1695c58ace4b192f9e6b9c8fe007cb40cb6adc3b Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 2 Oct 2022 13:42:11 +0900 Subject: [PATCH 27/31] Optimize concurrency task cancellation --- Sources/APIKit/Concurrency/Concurrency.swift | 15 ++++--- Sources/APIKit/Error/SessionTaskError.swift | 3 -- Sources/APIKit/Session.swift | 42 ++++++++++--------- .../Concurrency/ConcurrencyTests.swift | 6 +-- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/Sources/APIKit/Concurrency/Concurrency.swift b/Sources/APIKit/Concurrency/Concurrency.swift index 2555fc92..c0518e35 100644 --- a/Sources/APIKit/Concurrency/Concurrency.swift +++ b/Sources/APIKit/Concurrency/Concurrency.swift @@ -22,15 +22,16 @@ public extension Session { let cancellationHandler = SessionTaskCancellationHandler() return try await withTaskCancellationHandler(operation: { return try await withCheckedThrowingContinuation { continuation in - guard !Task.isCancelled else { - continuation.resume(throwing: SessionTaskError.taskAlreadyCancelledError) - return - } Task { - let sessionTask = send(request, callbackQueue: callbackQueue) { result in + let sessionTask = createSessionTask(request, callbackQueue: callbackQueue) { result in continuation.resume(with: result) } await cancellationHandler.register(with: sessionTask) + if await cancellationHandler.isTaskCancelled { + sessionTask?.cancel() + } else { + sessionTask?.resume() + } } } }, onCancel: { @@ -42,15 +43,17 @@ public extension Session { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private actor SessionTaskCancellationHandler { private var sessionTask: SessionTask? + private(set) var isTaskCancelled = false func register(with task: SessionTask?) { + guard !isTaskCancelled else { return } guard sessionTask == nil else { return } sessionTask = task } func cancel() { + isTaskCancelled = true sessionTask?.cancel() - sessionTask = nil } } diff --git a/Sources/APIKit/Error/SessionTaskError.swift b/Sources/APIKit/Error/SessionTaskError.swift index c019714d..864d9287 100644 --- a/Sources/APIKit/Error/SessionTaskError.swift +++ b/Sources/APIKit/Error/SessionTaskError.swift @@ -10,7 +10,4 @@ public enum SessionTaskError: Error { /// Error while creating `Request.Response` from `(Data, URLResponse)`. case responseError(Error) - - /// Error when the `Task` in Concurrency was cancelled before execution. - case taskAlreadyCancelledError } diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 4406fca9..85fb9615 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -55,8 +55,30 @@ open class Session { /// - returns: The new session task. @discardableResult open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - let callbackQueue = callbackQueue ?? self.callbackQueue + let task = createSessionTask(request, callbackQueue: callbackQueue, handler: handler) + task?.resume() + return task + } + + /// Cancels requests that passes the test. + /// - parameter requestType: The request type to cancel. + /// - parameter test: The test closure that determines if a request should be cancelled or not. + open func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { + adapter.getTasks { [weak self] tasks in + tasks + .filter { task in + if let request = self?.requestForTask(task) as Request? { + return test(request) + } else { + return false + } + } + .forEach { $0.cancel() } + } + } + internal func createSessionTask(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { + let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest do { urlRequest = try request.buildURLRequest() @@ -91,28 +113,10 @@ open class Session { } setRequest(request, forTask: task) - task.resume() return task } - /// Cancels requests that passes the test. - /// - parameter requestType: The request type to cancel. - /// - parameter test: The test closure that determines if a request should be cancelled or not. - open func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool = { _ in true }) { - adapter.getTasks { [weak self] tasks in - tasks - .filter { task in - if let request = self?.requestForTask(task) as Request? { - return test(request) - } else { - return false - } - } - .forEach { $0.cancel() } - } - } - private func setRequest(_ request: Request, forTask task: SessionTask) { objc_setAssociatedObject(task, &taskRequestKey, request, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } diff --git a/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift index 39b51c2a..30052335 100644 --- a/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift +++ b/Tests/APIKitTests/Concurrency/ConcurrencyTests.swift @@ -50,10 +50,10 @@ final class ConcurrencyTests: XCTestCase { XCTFail() } catch { let sessionError = try XCTUnwrap(error as? SessionTaskError) - switch sessionError { - case .taskAlreadyCancelledError: + if case .connectionError(let connectionError as NSError) = sessionError { + XCTAssertEqual(connectionError.code, 0) XCTAssertTrue(Task.isCancelled) - default: + } else { XCTFail() } } From 18e3611e543694d508e5956cda3f10324c401916 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 2 Oct 2022 14:53:32 +0900 Subject: [PATCH 28/31] [CI] Add Xcode14 and Swift 5.7 --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f8a1880..431dc0ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: macos-12 name: CocoaPods Lint env: - DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_14.0.1.app/Contents/Developer" steps: - uses: actions/checkout@v2 - run: pod lib lint --allow-warnings @@ -24,6 +24,9 @@ jobs: fail-fast: false matrix: include: + - xcode: "Xcode_14.0.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 14.0.1, Swift 5.7" - xcode: "Xcode_13.4.1" runsOn: macOS-12 name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" @@ -56,6 +59,10 @@ jobs: fail-fast: false matrix: include: + - xcode: "Xcode_14.0.1" + runsOn: macOS-12 + name: "macOS 12, Xcode 14.0.1, Swift 5.7" + action: swift test -c debug - xcode: "Xcode_13.4.1" runsOn: macOS-12 name: "macOS 12, Xcode 13.4.1, Swift 5.6.1" From 77fb3c5ec7ded310399a933da0521a153f03b36b Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 2 Oct 2022 15:08:18 +0900 Subject: [PATCH 29/31] [CI] Revert CocoaPods Lint execution to Xcode13.4.1 https://github.com/CocoaPods/CocoaPods/issues/11558 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 431dc0ae..9a0800f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: macos-12 name: CocoaPods Lint env: - DEVELOPER_DIR: "/Applications/Xcode_14.0.1.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" steps: - uses: actions/checkout@v2 - run: pod lib lint --allow-warnings From b839e53b870104798035b279d2a6168b0a2227b1 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sun, 2 Oct 2022 15:40:08 +0900 Subject: [PATCH 30/31] Set version 5.4.0 --- APIKit.podspec | 2 +- Sources/APIKit/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/APIKit.podspec b/APIKit.podspec index 7afab61b..f93510f8 100644 --- a/APIKit.podspec +++ b/APIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "APIKit" - s.version = "5.3.0" + s.version = "5.4.0" s.summary = "Type-safe networking abstraction layer that associates request type with response type." s.homepage = "https://github.com/ishkawa/APIKit" diff --git a/Sources/APIKit/Info.plist b/Sources/APIKit/Info.plist index 2b29ada6..9b01e54a 100644 --- a/Sources/APIKit/Info.plist +++ b/Sources/APIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.3.0 + 5.4.0 CFBundleSignature ???? CFBundleVersion From 528ad4e4e601e79bf5665dfa3ad76c3496afa387 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Sat, 22 Oct 2022 16:52:21 +0900 Subject: [PATCH 31/31] Remove deprecated NSKeyedArchiver / NSKeyedUnarchiver in tests --- .../BodyParametersType/ProtobufBodyParametersTests.swift | 8 +++----- .../DataParserType/ProtobufDataParserTests.swift | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift index 3f5da8b4..ccbc2515 100644 --- a/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift +++ b/Tests/APIKitTests/BodyParametersType/ProtobufBodyParametersTests.swift @@ -4,7 +4,7 @@ import APIKit class ProtobufBodyParametersTests: XCTestCase { func testProtobufSuccess() throws { - let object = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) + let object = try XCTUnwrap("data".data(using: .utf8)) let parameters = ProtobufBodyParameters(protobufObject: object) XCTAssertEqual(parameters.contentType, "application/protobuf") @@ -12,9 +12,7 @@ class ProtobufBodyParametersTests: XCTestCase { XCTFail() return } - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) + let string = String(data: data, encoding: .utf8) + XCTAssertEqual(string, "data") } } diff --git a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift index 6752bee9..63e91853 100644 --- a/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift +++ b/Tests/APIKitTests/DataParserType/ProtobufDataParserTests.swift @@ -9,13 +9,11 @@ class ProtobufDataParserTests: XCTestCase { } func testProtobufSuccess() throws { - let data = NSKeyedArchiver.archivedData(withRootObject: ["foo": 1, "bar": 2, "baz": 3]) + let data = try XCTUnwrap("data".data(using: .utf8)) let parser = ProtobufDataParser() let object = try XCTUnwrap(try parser.parse(data: data) as? Data) - let dictionary = NSKeyedUnarchiver.unarchiveObject(with: object) as? [String: Int] - XCTAssertEqual(dictionary?["foo"], 1) - XCTAssertEqual(dictionary?["bar"], 2) - XCTAssertEqual(dictionary?["baz"], 3) + let string = String(data: object, encoding: .utf8) + XCTAssertEqual(string, "data") } }