From 924f03eb54bdb8151e9666c3969254fd29fe800c Mon Sep 17 00:00:00 2001 From: Michael Watts <103135+mikwat@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:33:49 -0700 Subject: [PATCH] Preparing for Swift 6 (#1) Bumping swift-tools-version to 5.9 and enabling StrictConcurrency=complete in preparation for Swift 6. --- .github/workflows/swift.yml | 6 +- Package.swift | 9 +- README.md | 71 ++++++------ Sources/MailerLiteAPIClient/APIClient.swift | 2 +- .../MailerLiteAPIClient/APIClientError.swift | 2 +- .../APIErrorResponse.swift | 2 +- .../MailerLiteAPIClient.swift | 38 +++---- .../Requests/ListSubscribersTest.swift | 62 +++++----- .../Requests/UpdateSubscriberTest.swift | 106 +++++++++--------- .../Requests/UpsertSubscriberTest.swift | 62 +++++----- 10 files changed, 176 insertions(+), 184 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index cda61a2..9bc2b81 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -11,10 +11,12 @@ on: jobs: build: - - runs-on: macos-latest + runs-on: macos-14 steps: + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "5.9" - uses: actions/checkout@v3 - name: Build run: swift build -v diff --git a/Package.swift b/Package.swift index 19619d6..c28127f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7.1 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -18,7 +18,12 @@ let package = Package( // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "MailerLiteAPIClient"), + name: "MailerLiteAPIClient", + swiftSettings: [ + /// Remove `=targeted` to use the default `complete`. + .enableExperimentalFeature("StrictConcurrency=complete") + ] + ), .testTarget( name: "MailerLiteAPIClientTests", dependencies: ["MailerLiteAPIClient"]), diff --git a/README.md b/README.md index 7debe57..960268f 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,14 @@ import MailerLiteAPIClient let mailerLite = MailerLiteAPIClient(apiKey: "your-api-key") -mailerLite.send(ListSubscribers()) { result in - switch result { - case .success(let response): - // TODO: handle response - let data = response.data { - print("Found \(data.count) subscribers") - } - case .failure(let error): - // TODO: handle error +do { + let response = mailerLite.send(ListSubscribers()) + // TODO: handle response + let data = response.data { + print("Found \(data.count) subscribers") } +} catch { + // TODO: handle error } ``` @@ -32,16 +30,15 @@ import MailerLiteAPIClient let mailerLite = MailerLiteAPIClient(apiKey: "your-api-key") -mailerLite.send(UpsertSubscriber(email: "dummy@example.com", fields: Subscriber.Fields(lastName: "Testerson"))) { result in - switch result { - case .success(let response): - // TODO: handle response - let data = response.data { - print("Subscriber \(data.email) upserted") - } - case .failure(let error): - // TODO: handle error +do { + let response = mailerLite.send(UpsertSubscriber(email: "dummy@example.com", fields: Subscriber.Fields(lastName: "Testerson"))) + // TODO: handle response + let data = response.data { + print("Subscriber \(data.email) upserted") } +} catch { + // TODO: handle error + switch result { } ``` @@ -51,27 +48,25 @@ import MailerLiteAPIClient let mailerLite = MailerLiteAPIClient(apiKey: "your-api-key") -mailerLite.send(UpdateSubscriber( - id: "31897397363737859", - fields: Subscriber.Fields( - lastName: nil, - name: "Dummy" - ), - groups: [ - "4243829086487936", - "14133878422767533", - "31985378335392975" - ] -))) { result in - switch result { - case .success(let response): - // TODO: handle response - let data = response.data { - print("Subscriber \(data.id) updated") - } - case .failure(let error): - // TODO: handle error +do { + let response = mailerLite.send(UpdateSubscriber( + id: "31897397363737859", + fields: Subscriber.Fields( + lastName: nil, + name: "Dummy" + ), + groups: [ + "4243829086487936", + "14133878422767533", + "31985378335392975" + ] + ))) + // TODO: handle response + let data = response.data { + print("Subscriber \(data.id) updated") } +} catch { + // TODO: handle error } ``` diff --git a/Sources/MailerLiteAPIClient/APIClient.swift b/Sources/MailerLiteAPIClient/APIClient.swift index f8fdee3..23fc788 100644 --- a/Sources/MailerLiteAPIClient/APIClient.swift +++ b/Sources/MailerLiteAPIClient/APIClient.swift @@ -10,5 +10,5 @@ import Foundation public typealias ResultCallback = (Result) -> Void protocol APIClient { - func send(_ request: T, completion: @escaping ResultCallback) async + func send(_ request: T) async throws -> T.Response } diff --git a/Sources/MailerLiteAPIClient/APIClientError.swift b/Sources/MailerLiteAPIClient/APIClientError.swift index 256b193..466afc0 100644 --- a/Sources/MailerLiteAPIClient/APIClientError.swift +++ b/Sources/MailerLiteAPIClient/APIClientError.swift @@ -7,7 +7,7 @@ import Foundation -enum APIClientError: Error, Equatable { +enum APIClientError: Error, Equatable, Sendable { case decoding, encoding, unknownResponse case parsing(Error) case response(APIErrorResponse) diff --git a/Sources/MailerLiteAPIClient/APIErrorResponse.swift b/Sources/MailerLiteAPIClient/APIErrorResponse.swift index 8fbfb8e..d4ca1d7 100644 --- a/Sources/MailerLiteAPIClient/APIErrorResponse.swift +++ b/Sources/MailerLiteAPIClient/APIErrorResponse.swift @@ -7,7 +7,7 @@ import Foundation -public struct APIErrorResponse: Decodable { +public struct APIErrorResponse: Decodable, Sendable { let message: String? let errors: [String: [String]]? } diff --git a/Sources/MailerLiteAPIClient/MailerLiteAPIClient.swift b/Sources/MailerLiteAPIClient/MailerLiteAPIClient.swift index 54c0ed2..1d639de 100644 --- a/Sources/MailerLiteAPIClient/MailerLiteAPIClient.swift +++ b/Sources/MailerLiteAPIClient/MailerLiteAPIClient.swift @@ -18,7 +18,7 @@ public class MailerLiteAPIClient: APIClient { self.urlSession = urlSession } - public func send(_ request: T, completion: @escaping ResultCallback) -> Void { + public func send(_ request: T) async throws -> T.Response { let endpoint = self.endpoint(for: request) var urlRequest = URLRequest(url: endpoint) urlRequest.httpMethod = request.method.rawValue.uppercased() @@ -30,32 +30,28 @@ public class MailerLiteAPIClient: APIClient { do { urlRequest.httpBody = try JSONEncoder().encode(request) } catch { - return completion(.failure(APIClientError.encoding)) + throw APIClientError.encoding } } - let task = urlSession.dataTask(with: urlRequest) { data, response, error in - if let error = error { - completion(.failure(error)) - } else if let data = data { - do { - let httpResponse = response as! HTTPURLResponse - switch httpResponse.statusCode { - case 200...299: - let apiResponse = try JSONDecoder().decode(T.Response.self, from: data) - completion(.success(apiResponse)) - default: - let apiErrorResponse = try JSONDecoder().decode(APIErrorResponse.self, from: data) - completion(.failure(APIClientError.response(apiErrorResponse))) - } - } catch { - completion(.failure(APIClientError.parsing(error))) - } + let (data, response) = try await urlSession.data(for: urlRequest) + do { + let httpResponse = response as! HTTPURLResponse + switch httpResponse.statusCode { + case 200...299: + let apiResponse = try JSONDecoder().decode(T.Response.self, from: data) + return apiResponse + default: + let apiErrorResponse = try JSONDecoder().decode(APIErrorResponse.self, from: data) + throw APIClientError.response(apiErrorResponse) + } + } catch { + if let error = error as? APIClientError { + throw error } else { - completion(.failure(APIClientError.unknownResponse)) + throw APIClientError.parsing(error) } } - task.resume() } private func endpoint(for request: T) -> URL { diff --git a/Tests/MailerLiteAPIClientTests/Requests/ListSubscribersTest.swift b/Tests/MailerLiteAPIClientTests/Requests/ListSubscribersTest.swift index 449451a..f8fb367 100644 --- a/Tests/MailerLiteAPIClientTests/Requests/ListSubscribersTest.swift +++ b/Tests/MailerLiteAPIClientTests/Requests/ListSubscribersTest.swift @@ -23,7 +23,7 @@ final class ListSubscribersTest: XCTestCase { expectation = expectation(description: "Expectation") } - func testSuccess() { + func testSuccess() async { let jsonString = """ { "data": [ @@ -82,21 +82,20 @@ final class ListSubscribersTest: XCTestCase { return (response, data) } - apiClient.send(ListSubscribers()) { result in - switch result { - case .success(let response): - XCTAssertEqual(response.data?.count, 1) - XCTAssertEqual(response.data?.first?.email, "dummy@example.com") - XCTAssertNotNil(response.meta?.nextCursor) - case .failure(let error): - XCTFail("Error was not expected: \(error)") - } - self.expectation.fulfill() + do { + let response = try await apiClient.send(ListSubscribers()) + XCTAssertEqual(response.data?.count, 1) + XCTAssertEqual(response.data?.first?.email, "dummy@example.com") + XCTAssertNotNil(response.meta?.nextCursor) + } catch { + XCTFail("Error was not expected: \(error)") } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } - func testFailure() { + func testFailure() async { let data = Data() let apiURL = URL(string: "https://connect.mailerlite.com/api/subscribers?")! MockURLProtocol.requestHandler = { request in @@ -104,26 +103,25 @@ final class ListSubscribersTest: XCTestCase { return (response, data) } - apiClient.send(ListSubscribers()) { result in - switch result { - case .success(_): - XCTFail("Success was not expected.") - case .failure(let error): - guard let apiError = error as? APIClientError else { - XCTFail("Incorrect error received.") - self.expectation.fulfill() - return - } - - switch apiError { - case .parsing(let error): - XCTAssertEqual(error.localizedDescription, "The data couldn’t be read because it isn’t in the correct format.") - default: - XCTFail("Incorrect error received.") - } + do { + let _ = try await apiClient.send(ListSubscribers()) + XCTFail("Success was not expected.") + } catch { + guard let apiError = error as? APIClientError else { + XCTFail("Incorrect error received.") + self.expectation.fulfill() + return + } + + switch apiError { + case .parsing(let error): + XCTAssertEqual(error.localizedDescription, "The data couldn’t be read because it isn’t in the correct format.") + default: + XCTFail("Incorrect error received.") } - self.expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } } diff --git a/Tests/MailerLiteAPIClientTests/Requests/UpdateSubscriberTest.swift b/Tests/MailerLiteAPIClientTests/Requests/UpdateSubscriberTest.swift index c08a673..2a77ddc 100644 --- a/Tests/MailerLiteAPIClientTests/Requests/UpdateSubscriberTest.swift +++ b/Tests/MailerLiteAPIClientTests/Requests/UpdateSubscriberTest.swift @@ -23,7 +23,7 @@ final class UpdateSubscriberTest: XCTestCase { expectation = expectation(description: "Expectation") } - func testSuccess() { + func testSuccess() async { let jsonString = """ { "data": { @@ -68,32 +68,31 @@ final class UpdateSubscriberTest: XCTestCase { return (response, data) } - apiClient.send(UpdateSubscriber( - id: "31897397363737859", - fields: Subscriber.Fields( - lastName: nil, - name: "Dummy" - ), - groups: [ - "4243829086487936", - "14133878422767533", - "31985378335392975" - ] - )) { result in - switch result { - case .success(let response): - XCTAssertNotNil(response.data) - XCTAssertEqual(response.data?.email, "dummy@example.com") - XCTAssertEqual(response.data?.fields?.name, "Dummy") - case .failure(let error): - XCTFail("Error was not expected: \(error)") - } - self.expectation.fulfill() + do { + let response = try await apiClient.send(UpdateSubscriber( + id: "31897397363737859", + fields: Subscriber.Fields( + lastName: nil, + name: "Dummy" + ), + groups: [ + "4243829086487936", + "14133878422767533", + "31985378335392975" + ] + )) + XCTAssertNotNil(response.data) + XCTAssertEqual(response.data?.email, "dummy@example.com") + XCTAssertEqual(response.data?.fields?.name, "Dummy") + } catch { + XCTFail("Error was not expected: \(error)") } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } - func testFailure() { + func testFailure() async { let jsonString = """ { "message": "The given data was invalid.", @@ -113,37 +112,36 @@ final class UpdateSubscriberTest: XCTestCase { return (response, data) } - apiClient.send(UpdateSubscriber( - id: "31897397363737859", - fields: Subscriber.Fields( - lastName: nil, - name: "Dummy" - ), - groups: [ - "4243829086487936", - "14133878422767533", - "31985378335392975" - ] - )) { result in - switch result { - case .success(let response): - XCTFail("Success was not expected: \(response)") - case .failure(let error): - guard let apiError = error as? APIClientError else { - XCTFail("Incorrect error received.") - self.expectation.fulfill() - return - } - - switch apiError { - case .response(let response): - XCTAssertEqual(response.message, "The given data was invalid.") - default: - XCTFail("Incorrect error received.") - } + do { + let response = try await apiClient.send(UpdateSubscriber( + id: "31897397363737859", + fields: Subscriber.Fields( + lastName: nil, + name: "Dummy" + ), + groups: [ + "4243829086487936", + "14133878422767533", + "31985378335392975" + ] + )) + XCTFail("Success was not expected: \(response)") + } catch { + guard let apiError = error as? APIClientError else { + XCTFail("Incorrect error received.") + self.expectation.fulfill() + return + } + + switch apiError { + case .response(let response): + XCTAssertEqual(response.message, "The given data was invalid.") + default: + XCTFail("Incorrect error received.") } - self.expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } } diff --git a/Tests/MailerLiteAPIClientTests/Requests/UpsertSubscriberTest.swift b/Tests/MailerLiteAPIClientTests/Requests/UpsertSubscriberTest.swift index 3b6ae15..ee243e2 100644 --- a/Tests/MailerLiteAPIClientTests/Requests/UpsertSubscriberTest.swift +++ b/Tests/MailerLiteAPIClientTests/Requests/UpsertSubscriberTest.swift @@ -23,7 +23,7 @@ final class UpsertSubscriberTest: XCTestCase { expectation = expectation(description: "Expectation") } - func testSuccess() { + func testSuccess() async { let jsonString = """ { "data": { @@ -68,21 +68,20 @@ final class UpsertSubscriberTest: XCTestCase { return (response, data) } - apiClient.send(UpsertSubscriber(email: "dummy@example.com")) { result in - switch result { - case .success(let response): - XCTAssertNotNil(response.data) - XCTAssertEqual(response.data?.email, "dummy@example.com") - XCTAssertEqual(response.data?.fields?.lastName, "Testerson") - case .failure(let error): - XCTFail("Error was not expected: \(error)") - } - self.expectation.fulfill() + do { + let response = try await apiClient.send(UpsertSubscriber(email: "dummy@example.com")) + XCTAssertNotNil(response.data) + XCTAssertEqual(response.data?.email, "dummy@example.com") + XCTAssertEqual(response.data?.fields?.lastName, "Testerson") + } catch { + XCTFail("Error was not expected: \(error)") } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } - func testFailure() { + func testFailure() async { let jsonString = """ { "message": "The given data was invalid.", @@ -102,26 +101,25 @@ final class UpsertSubscriberTest: XCTestCase { return (response, data) } - apiClient.send(UpsertSubscriber(email: "dummy.example.com")) { result in - switch result { - case .success(let response): - XCTFail("Success was not expected: \(response)") - case .failure(let error): - guard let apiError = error as? APIClientError else { - XCTFail("Incorrect error received.") - self.expectation.fulfill() - return - } - - switch apiError { - case .response(let response): - XCTAssertEqual(response.message, "The given data was invalid.") - default: - XCTFail("Incorrect error received.") - } + do { + let response = try await apiClient.send(UpsertSubscriber(email: "dummy.example.com")) + XCTFail("Success was not expected: \(response)") + } catch { + guard let apiError = error as? APIClientError else { + XCTFail("Incorrect error received.") + self.expectation.fulfill() + return + } + + switch apiError { + case .response(let response): + XCTAssertEqual(response.message, "The given data was invalid.") + default: + XCTFail("Incorrect error received.") } - self.expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + + self.expectation.fulfill() + await fulfillment(of: [expectation], timeout: 1.0) } }