Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/1.0.0 #10

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/lintEditedFiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ git fetch origin main
echo "Fetched"

# Use a conditional to check if there are edited files
if EDITED_FILES=$(git diff HEAD origin/main --name-only --diff-filter=d | grep "\.swift" | grep -v "\.swiftlint\.yml" | xargs echo | tr ' ' ','); then
if EDITED_FILES=$(git diff HEAD origin/main --name-only --diff-filter=d | grep "\.swift" | grep -v "\.swiftlint\.yml" | xargs echo | tr ' ' ',' | sed 's/,/, /g'); then
echo "Got edited files"
echo $EDITED_FILES

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/verify-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- name: Build
- name: Build the Package
run: |
set -o pipefail && xcodebuild -scheme CheckoutNetwork -destination "platform=iOS Simulator,name=iPhone 14 Pro,OS=latest"

- name: Run Tests
run: |
set -o pipefail && xcodebuild -scheme CheckoutNetwork -destination "platform=iOS Simulator,name=iPhone 14 Pro,OS=latest" test
55 changes: 55 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
included:
- Source

excluded:
- Tests

opt_in_rules:
- array_init
- closure_end_indentation
- closure_spacing
- collection_alignment
- colon # promote to error
- convenience_type
- discouraged_object_literal
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- fatal_error_message
- first_where
- force_unwrapping
- implicitly_unwrapped_optional
- last_where
- legacy_random
- literal_expression_end_indentation
- multiline_arguments
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- prefer_self_type_over_type_of_self
- redundant_nil_coalescing
- redundant_type_annotation
- strict_fileprivate
- toggle_bool
- trailing_closure
- unneeded_parentheses_in_closure_argument
- yoda_condition

disabled_rules:
- multiline_parameters_brackets

analyzer_rules:
- unused_import

line_length:
warning: 160
ignores_urls: true

identifier_name:
excluded:
- id
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "CheckoutNetwork",
platforms: [
.iOS(.v11),
.iOS(.v13),
],
products: [
.library(
Expand All @@ -19,12 +19,14 @@ let package = Package(
],
targets: [
.target(
name: "CheckoutNetwork"),
name: "CheckoutNetwork",
path: "Sources/CheckoutNetwork"),
.target(
name: "CheckoutNetworkFakeClient",
dependencies: ["CheckoutNetwork"]),
.testTarget(
name: "CheckoutNetworkTests",
dependencies: ["CheckoutNetwork"]),
dependencies: ["CheckoutNetwork"],
path: "Tests"),
]
)
5 changes: 4 additions & 1 deletion Sources/CheckoutNetwork/CheckoutClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public protocol CheckoutClientInterface {
/// Create, customise and run a request with the given configuration, calling the completion handler once completed
func runRequest<T: Decodable>(with configuration: RequestConfiguration,
completionHandler: @escaping CompletionHandler<T>)


/// Async wrapper of func runRequest(_:_:) with CompletionHandler<T>
func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T

/// Create, customise and run a request with the given configuration, calling the completion handler once completed
func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler)
Expand Down
40 changes: 32 additions & 8 deletions Sources/CheckoutNetwork/CheckoutNetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
completionHandler(.failure(error))
return
}
if let responseError = self.getErrorFromResponse(response) {
completionHandler(.failure(responseError))
return
}

guard let data = data else {
completionHandler(.failure(CheckoutNetworkError.noDataResponseReceived))
return
}

if let responseError = self.getErrorFromResponse(response, data: data) {
completionHandler(.failure(responseError))
return
}

do {
let dataResponse = try JSONDecoder().decode(T.self, from: data)
completionHandler(.success(dataResponse))
Expand All @@ -55,7 +57,20 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
task.resume()
}
}


public func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (result: Result<T, Error>) in
switch result {
case .success(let response):
continuation.resume(returning: response)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

public func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler) {
taskQueue.sync {
Expand All @@ -68,7 +83,7 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
completionHandler(error)
return
}
if let responseError = self.getErrorFromResponse(response) {
if let responseError = self.getErrorFromResponse(response, data: nil) {
completionHandler(responseError)
return
}
Expand All @@ -87,11 +102,20 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
return taskID
}

private func getErrorFromResponse(_ response: URLResponse?) -> Error? {
guard let response = response as? HTTPURLResponse else {
private func getErrorFromResponse(_ response: URLResponse?, data: Data?) -> Error? {
guard let response = response as? HTTPURLResponse else {
return CheckoutNetworkError.unexpectedResponseCode(code: 0)
}

guard response.statusCode != 422 else {
do {
let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data())
return CheckoutNetworkError.invalidData(reason: errorReason)
} catch {
return CheckoutNetworkError.invalidDataResponseReceivedWithNoData
}
}

guard response.statusCode >= 200,
response.statusCode < 300 else {
return CheckoutNetworkError.unexpectedResponseCode(code: response.statusCode)
Expand Down
7 changes: 7 additions & 0 deletions Sources/CheckoutNetwork/CheckoutNetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ public enum CheckoutNetworkError: Error, Equatable {

/// Network call and completion appear valid but no data was returned making the parsing impossible. Use different call if no data is expected
case noDataResponseReceived

/// Network response returned with HTTP Code 422
case invalidData(reason: ErrorReason)


/// HTTP code 422 received with no meaningful data alongside
case invalidDataResponseReceivedWithNoData
}
20 changes: 20 additions & 0 deletions Sources/CheckoutNetwork/Models/ErrorReason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// ErrorReason.swift
//
//
// Created by Okhan Okbay on 18/01/2024.
//

import Foundation

public struct ErrorReason: Decodable, Equatable {
public let requestID: String
public let errorType: String
public let errorCodes: [String]

enum CodingKeys: String, CodingKey {
case requestID = "request_id"
case errorType = "error_type"
case errorCodes = "error_codes"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ import Foundation
import CheckoutNetwork

final public class CheckoutNetworkFakeClient: CheckoutClientInterface {

public var calledRequests: [(config: RequestConfiguration, completion: Any)] = []


public var calledAsyncRequests: [RequestConfiguration] = []
public var dataToBeReturned: Decodable!

public func runRequest<T: Decodable>(with configuration: RequestConfiguration,
completionHandler: @escaping CompletionHandler<T>) {
calledRequests.append((config: configuration, completion: completionHandler))
}


public func runRequest<T: Decodable>(with configuration: CheckoutNetwork.RequestConfiguration) async throws -> T {
calledAsyncRequests.append(configuration)
return dataToBeReturned as! T
}

public func runRequest(with configuration: RequestConfiguration,
completionHandler: @escaping NoDataResponseCompletionHandler) {
calledRequests.append((configuration, completionHandler))
}

}
49 changes: 49 additions & 0 deletions Tests/CheckoutNetworkTests/AsyncWrapperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// AsyncWrapperTests.swift
//
//
// Created by Okhan Okbay on 18/01/2024.
//

@testable import CheckoutNetwork
import XCTest

final class AsyncWrapperTests: XCTestCase {

func test_whenRunRequestReturnsData_ThenAsyncRunRequestPropagatesIt() async throws {
let fakeSession = FakeSession()
let fakeDataTask = FakeDataTask()
fakeSession.calledDataTasksReturn = fakeDataTask
let client = CheckoutNetworkClientSpy(session: fakeSession)
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

let expectedResult = FakeObject(id: "some response")
client.expectedResult = expectedResult
client.expectedError = nil
let result: FakeObject = try await client.runRequest(with: testConfig)
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
XCTAssertEqual(result, expectedResult)
}

func test_whenRunRequestReturnsError_ThenAsyncRunRequestPropagatesIt() async throws {
let fakeSession = FakeSession()
let fakeDataTask = FakeDataTask()
fakeSession.calledDataTasksReturn = fakeDataTask
let client = CheckoutNetworkClientSpy(session: fakeSession)
let testConfig = try! RequestConfiguration(path: FakePath.testServices)

let expectedError = FakeError.someError
client.expectedResult = nil
client.expectedError = expectedError

do {
let _: FakeObject = try await client.runRequest(with: testConfig)
XCTFail("An error was expected to be thrown")
} catch let error as FakeError {
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
XCTAssertEqual(error, expectedError)
}
}
}
4 changes: 2 additions & 2 deletions Tests/CheckoutNetworkTests/CheckoutNetworkClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class CheckoutNetworkClientTests: XCTestCase {

let testConfig = try! RequestConfiguration(path: FakePath.testServices)
client.runRequest(with: testConfig) { (result: Result<FakeObject, Error>) in }

XCTAssertEqual(client.tasks.count, 1)
XCTAssertTrue(client.tasks.values.first === fakeDataTask)
XCTAssertTrue(fakeDataTask.wasStarted)
Expand Down Expand Up @@ -73,7 +73,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
XCTAssertEqual(failure as NSError, expectedError)
}
}

XCTAssertFalse(client.tasks.isEmpty)
let requestCompletion = fakeSession.calledDataTasks.first!.completion
requestCompletion(expectedData, expectedResponse, expectedError)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// CheckoutNetworkClientSpy.swift
//
//
// Created by Okhan Okbay on 19/01/2024.
//

@testable import CheckoutNetwork

class CheckoutNetworkClientSpy: CheckoutNetworkClient {
private(set) var runRequestCallCount: Int = 0
private(set) var configuration: RequestConfiguration!

var expectedResult: FakeObject?
var expectedError: Error?

override func runRequest<T>(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.CompletionHandler<T>) where T : Decodable {
runRequestCallCount += 1
self.configuration = configuration

if let result = expectedResult {
completionHandler(.success(result as! T))
} else if let error = expectedError {
completionHandler(.failure(error))
}
}
}
12 changes: 12 additions & 0 deletions Tests/CheckoutNetworkTests/Helpers/FakeError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// FakeError.swift
//
//
// Created by Okhan Okbay on 19/01/2024.
//

import Foundation

enum FakeError: Error {
case someError
}