From 757242f9507e906a350d3329028118e6782bbcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 29 Sep 2021 14:57:34 +0200 Subject: [PATCH 1/5] Add new async-await based response methods --- Sources/Microya/Core/ApiProvider.swift | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Sources/Microya/Core/ApiProvider.swift b/Sources/Microya/Core/ApiProvider.swift index 00797bd..92cfd85 100644 --- a/Sources/Microya/Core/ApiProvider.swift +++ b/Sources/Microya/Core/ApiProvider.swift @@ -137,6 +137,68 @@ open class ApiProvider { } } + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + public func response(on endpoint: EndpointType) async -> TypedResult { + await self.response(on: endpoint, decodeBodyTo: EmptyBodyResponse.self) + } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + public func response( + on endpoint: EndpointType, + decodeBodyTo: ResultType.Type + ) async -> TypedResult { + var request: URLRequest = endpoint.buildRequest(baseUrl: baseUrl) + + for plugin in plugins { + plugin.modifyRequest(&request, endpoint: endpoint) + } + + for plugin in plugins { + plugin.willPerformRequest(request, endpoint: endpoint) + } + + func handleResponse(data: Data?, response: URLResponse?, error: Error?) -> TypedResult { + let urlSessionResult: URLSessionResult = (data: data, response: response, error: error) + let typedResult: TypedResult = self.decodeBody(from: urlSessionResult, endpoint: endpoint) + + for plugin in self.plugins { + plugin.didPerformRequest(urlSessionResult: urlSessionResult, typedResult: typedResult, endpoint: endpoint) + } + + return typedResult + } + + if let mockingBehavior = mockingBehavior { + let baseUrl = self.baseUrl + + var schedulerFired: Bool = false + mockingBehavior.scheduler.schedule(after: mockingBehavior.scheduler.now.advanced(by: mockingBehavior.delay)) { + schedulerFired = true + } + while !schedulerFired { /* wait for scheduler to schedule */ } + + guard let mockedResponse = mockingBehavior.mockedResponseProvider(endpoint) else { + return .failure(.emptyMockedResponse) + } + + return handleResponse( + data: mockedResponse.bodyData, + response: mockedResponse.httpUrlResponse(baseUrl: baseUrl), + error: nil + ) + } + else { + // this is the main logic, making the actual call + do { + let (data, response) = try await URLSession.shared.data(for: request) + return handleResponse(data: data, response: response, error: nil) + } + catch { + return handleResponse(data: nil, response: nil, error: error) + } + } + } + /// Performs the asynchronous request for the chosen write-only endpoint and calls the completion closure with the result. /// Returns a `EmptyBodyResponse` on success. /// From 93361e7e645e6d270aa6e1be39755ee5f7536a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 30 Sep 2021 13:20:11 +0200 Subject: [PATCH 2/5] Add tests for new async-await based response API --- .../MicroyaIntegrationTests.swift | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/Tests/MicroyaTests/MicroyaIntegrationTests.swift b/Tests/MicroyaTests/MicroyaIntegrationTests.swift index a34199d..c2e992f 100755 --- a/Tests/MicroyaTests/MicroyaIntegrationTests.swift +++ b/Tests/MicroyaTests/MicroyaIntegrationTests.swift @@ -466,4 +466,128 @@ class MicroyaIntegrationTests: XCTestCase { wait(for: [expectation], timeout: 10) #endif } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testIndexAsync() async throws { + let typedResponseBody = + try await sampleApiProvider.response( + on: .index(sortedBy: "updatedAt"), + decodeBodyTo: PostmanEchoResponse.self + ) + .get() + + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept-Language"], "en") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Authorization"], "Basic abc123") + + XCTAssertEqual(TestDataStore.request?.httpMethod, "GET") + XCTAssertEqual(TestDataStore.request?.url?.path, "/get") + XCTAssertEqual(TestDataStore.request?.url?.query, "sortedBy=updatedAt") + + XCTAssertNotNil(TestDataStore.urlSessionResult?.data) + XCTAssertNil(TestDataStore.urlSessionResult?.error) + XCTAssertNotNil(TestDataStore.urlSessionResult?.response) + + XCTAssertEqual(typedResponseBody.args, ["sortedBy": "updatedAt"]) + XCTAssertEqual(typedResponseBody.headers["content-type"], "application/json") + XCTAssertEqual(typedResponseBody.headers["accept"], "application/json") + XCTAssertEqual(typedResponseBody.headers["accept-language"], "en") + XCTAssertEqual(typedResponseBody.url, "https://postman-echo.com/get?sortedBy=updatedAt") + } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testPostAsync() async throws { + let typedResponseBody = + try await sampleApiProvider.response( + on: .post(fooBar: FooBar(foo: "Lorem", bar: "Ipsum")), + decodeBodyTo: PostmanEchoResponse.self + ) + .get() + + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept-Language"], "en") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Authorization"], "Basic abc123") + + XCTAssertEqual(TestDataStore.request?.httpMethod, "POST") + XCTAssertEqual(TestDataStore.request?.url?.path, "/post") + XCTAssertNil(TestDataStore.request?.url?.query) + + XCTAssertNotNil(TestDataStore.urlSessionResult?.data) + XCTAssertNil(TestDataStore.urlSessionResult?.error) + XCTAssertNotNil(TestDataStore.urlSessionResult?.response) + + XCTAssertEqual(typedResponseBody.args, [:]) + XCTAssertEqual(typedResponseBody.headers["content-type"], "application/json") + XCTAssertEqual(typedResponseBody.headers["accept"], "application/json") + XCTAssertEqual(typedResponseBody.headers["accept-language"], "en") + XCTAssertEqual(typedResponseBody.url, "https://postman-echo.com/post") + } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testGetAsync() async { + let response = await sampleApiProvider.response( + on: .get(fooBarID: fooBarID), + decodeBodyTo: PostmanEchoResponse.self + ) + XCTAssertThrowsError(try response.get()) + + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept-Language"], "en") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Authorization"], "Basic abc123") + + XCTAssertEqual(TestDataStore.request?.httpMethod, "GET") + XCTAssertEqual(TestDataStore.request?.url?.path, "/get/\(fooBarID)") + XCTAssertNil(TestDataStore.request?.url?.query) + } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testPatchAsync() async throws { + let response = await sampleApiProvider.response( + on: .patch(fooBarID: fooBarID, fooBar: FooBar(foo: "Dolor", bar: "Amet")), + decodeBodyTo: PostmanEchoResponse.self + ) + XCTAssertThrowsError(try response.get()) + + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept-Language"], "en") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Authorization"], "Basic abc123") + + XCTAssertEqual(TestDataStore.request?.httpMethod, "PATCH") + XCTAssertEqual(TestDataStore.request?.url?.path, "/patch/\(fooBarID)") + XCTAssertNil(TestDataStore.request?.url?.query) + + XCTAssertNotNil(TestDataStore.urlSessionResult?.data) + XCTAssertNil(TestDataStore.urlSessionResult?.error) + XCTAssertNotNil(TestDataStore.urlSessionResult?.response) + } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testDeleteAsync() async throws { + let result = await sampleApiProvider.response(on: .delete) + + switch result { + case .success: + break + + default: + XCTFail("Expected request to succeed.") + } + + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Accept-Language"], "en") + XCTAssertEqual(TestDataStore.request?.allHTTPHeaderFields?["Authorization"], "Basic abc123") + + XCTAssertEqual(TestDataStore.request?.httpMethod, "DELETE") + XCTAssertEqual(TestDataStore.request?.url?.path, "/delete") + XCTAssertNil(TestDataStore.request?.url?.query) + + XCTAssertNotNil(TestDataStore.urlSessionResult?.data) + XCTAssertNil(TestDataStore.urlSessionResult?.error) + XCTAssertNotNil(TestDataStore.urlSessionResult?.response) + } } From 397e5d02f8fb845ee380fb6822d03e62126d6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 30 Sep 2021 13:23:53 +0200 Subject: [PATCH 3/5] Remove no longer necessary LinuxMain file Newer versions of Swift (package manager) use automated test discovery on Linux by default. --- .sourcery/LinuxMain.stencil | 16 ---------------- Tests/LinuxMain.swift | 30 ------------------------------ build-script.sh | 1 - 3 files changed, 47 deletions(-) delete mode 100644 .sourcery/LinuxMain.stencil delete mode 100644 Tests/LinuxMain.swift diff --git a/.sourcery/LinuxMain.stencil b/.sourcery/LinuxMain.stencil deleted file mode 100644 index 19a3deb..0000000 --- a/.sourcery/LinuxMain.stencil +++ /dev/null @@ -1,16 +0,0 @@ -@testable import MicroyaTests -import XCTest - -// swiftlint:disable line_length file_length - -{% for type in types.classes|based:"XCTestCase" %} -extension {{ type.name }} { - static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [ - {% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %} - {% endfor %}] -} - -{% endfor %} -XCTMain([ -{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %} -{% endfor %}]) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 604721f..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,30 +0,0 @@ -// Generated using Sourcery 1.0.3 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT - -@testable import MicroyaTests -import XCTest - -// swiftlint:disable line_length file_length - -extension MicroyaIntegrationTests { - static var allTests: [(String, (MicroyaIntegrationTests) -> () throws -> Void)] = [ - ("testIndex", testIndex), - ("testPost", testPost), - ("testGet", testGet), - ("testPatch", testPatch), - ("testDelete", testDelete), - ("testIndexCombine", testIndexCombine), - ("testPostCombine", testPostCombine), - ("testGetCombine", testGetCombine), - ("testPatchCombine", testPatchCombine), - ("testDeleteCombine", testDeleteCombine), - ("testMockedGet", testMockedGet), - ("testMockedPostCombine", testMockedPostCombine), - ("testMockedGetCombine", testMockedGetCombine), - ("testMockedDeleteCombine", testMockedDeleteCombine), - ] -} - -XCTMain([ - testCase(MicroyaIntegrationTests.allTests) -]) diff --git a/build-script.sh b/build-script.sh index c9e2caa..b2636d0 100755 --- a/build-script.sh +++ b/build-script.sh @@ -1,6 +1,5 @@ #!/bin/bash set -euxo pipefail -sourcery --sources Tests/MicroyaTests --templates .sourcery/LinuxMain.stencil --output Tests/LinuxMain.swift swift-format --recursive Sources/Microya Tests --in-place swift-format lint --recursive Sources/Microya Tests From 282ac27f39f4428e1e4762601a250c8e87652199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 30 Sep 2021 13:25:21 +0200 Subject: [PATCH 4/5] Documented changes of PR #6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27b81d..738b226 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] ### Added -- None. +- New async `response` method based on the new concurrency features available in Swift 5.5. ### Changed - None. ### Deprecated From 7f62f46695371073521602237744bd5a8ac7ba84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 30 Sep 2021 13:30:54 +0200 Subject: [PATCH 5/5] Bump version num & finalize new changelog section --- CHANGELOG.md | 6 +++++- README.md | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738b226..60de246 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] ### Added -- New async `response` method based on the new concurrency features available in Swift 5.5. +- None. ### Changed - None. ### Deprecated @@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ### Security - None. +## [0.6.0] - 2021-09-30 +### Added +- New async `response` method based on the new concurrency features available in Swift 5.5. + ## [0.5.0] - 2021-09-28 ### Added - New `mockingBehavior` parameter on `ApiProvider` for testing purposes. Specify one with `delay` and `scheduler`, e.g. `.seconds(0.5)` and `DispatchQueue.main.eraseToAnyScheduler()`. Provides `nil` by default to make actual requests. diff --git a/README.md b/README.md index b5d2165..513875e 100755 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ alt="codebeat badge"> - Version: 0.5.0 + Version: 0.6.0 Swift: 5.3