Skip to content
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 Projects/Detail/Sources/Service/DetailService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum DetailServiceError: Error, Equatable {
}

final class DetailService: Sendable {
private nonisolated(unsafe) let networkService: NetworkServiceProtocol
private let networkService: NetworkServiceProtocol

init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
Expand Down
14 changes: 9 additions & 5 deletions Projects/Detail/Tests/Doubles/NetworkServiceProtocolSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
import Foundation
import NetworkingInterfaces

final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendable {
enum Method: Equatable {
actor NetworkServiceProtocolSpy: NetworkServiceProtocol, Sendable {
enum Method: Equatable, Sendable {
case request
}

var calledMethods: [Method] = []
var requestResult: Result<(Data, HTTPURLResponse), Error>?
private(set) var calledMethods: [Method] = []
private(set) var requestResult: Result<(Data, HTTPURLResponse), NetworkError>?

func request(endpoint: APIEndpointProtocol) async throws -> (Data, HTTPURLResponse) {
func request(endpoint: APIEndpointProtocol) async throws(NetworkError) -> (Data, HTTPURLResponse) {
calledMethods.append(.request)

guard let result = requestResult else {
Expand All @@ -26,4 +26,8 @@ final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendab

return try result.get()
}

func setResponse(requestResult: Result<(Data, HTTPURLResponse), NetworkError>) async {
self.requestResult = requestResult
}
}
12 changes: 6 additions & 6 deletions Projects/Detail/Tests/Service/DetailServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ struct DetailServiceTests {
headerFields: nil
))

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

let result = try await sut.fetchExchangeAssets(id: 1)

#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
#expect(result.count == 2)
#expect(result[0] == Asset(id: 1, name: "Bitcoin", symbol: "BTC", priceUsd: 50000.0))
#expect(result[1] == Asset(id: 2, name: "Ethereum", symbol: "ETH", priceUsd: 3000.0))
Expand Down Expand Up @@ -91,7 +91,7 @@ struct DetailServiceTests {
headerFields: nil
))

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

do {
_ = try await sut.fetchExchangeAssets(id: 999)
Expand All @@ -104,7 +104,7 @@ struct DetailServiceTests {
elapsed: 10,
creditCount: 1
)))
#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
}
}

Expand All @@ -123,13 +123,13 @@ struct DetailServiceTests {
headerFields: nil
))

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

do {
_ = try await sut.fetchExchangeAssets(id: 1)
Issue.record("Expected error to be thrown")
} catch {
#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions Projects/Home/Sources/Protocols/HomeServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import HomeInterfaces
import Foundation

protocol HomeServiceProtocol: AnyObject, Sendable {
func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary]
func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail]
@concurrent
func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary]
@concurrent
func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail]
}
31 changes: 23 additions & 8 deletions Projects/Home/Sources/Service/HomeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,26 @@ import NetworkingInterfaces

enum ServiceError: Error, Equatable {
case decodeFail(String?)
case requestError(String?)
case network(Status)
}

final class HomeService: Sendable {
private nonisolated(unsafe) let networkService: NetworkServiceProtocol
private let networkService: NetworkServiceProtocol

init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}

private func performRequest<T: Codable>(endpoint: APIEndpointProtocol) async throws -> T {
let (data, httpUrlResponse) = try await networkService.request(endpoint: endpoint)
@concurrent
private func performRequest<T: Codable>(endpoint: APIEndpointProtocol) async throws(ServiceError) -> T {
let (data, httpUrlResponse): (Data, HTTPURLResponse)

do {
(data, httpUrlResponse) = try await networkService.request(endpoint: endpoint)
} catch {
throw ServiceError.requestError(error.localizedDescription)
}

switch httpUrlResponse.statusCode {
case 200...299:
Expand All @@ -34,20 +42,27 @@ final class HomeService: Sendable {
}
}

func decodeResponse<T: Codable>(_ data: Data) throws -> T {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
func decodeResponse<T: Codable>(_ data: Data) throws(ServiceError) -> T {
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
throw ServiceError.decodeFail(error.localizedDescription)
}

}
}

extension HomeService: HomeServiceProtocol {
func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary] {
@concurrent
func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] {
let endpoint = HomeEndpoint.fetchItems(page: page, limit: limit)
let response: ExchangeResponse<ExchangeSummary> = try await performRequest(endpoint: endpoint)
return response.data
}

func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail] {
@concurrent
func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] {
let endpoint = HomeEndpoint.fetchDetail(ids: ids)
let response: ExchangeDetailResponse<ExchangeDetail> = try await performRequest(endpoint: endpoint)
return Array(response.data.values)
Expand Down
33 changes: 14 additions & 19 deletions Projects/Home/Sources/ViewModel/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class HomeViewModel: HomeViewModelProtocol {
weak var coordinatorDelegate: HomeViewModelCoordinatorDelegate?

private let service: HomeServiceProtocol
private let defaultMessage = "Failed to load data. Press try again or check your connection."

private var currentPage = 1
private var isFetching = false
Expand Down Expand Up @@ -83,27 +84,21 @@ final class HomeViewModel: HomeViewModelProtocol {
isFetching = false
state = .loaded

} catch {
isFetching = false
if exchanges.isEmpty {
let serviceError = error as? ServiceError
let defaultMessage = "Failed to Load data. Press try again later or check your connection."
let message: String
var code: String?
switch serviceError {
case .decodeFail, .none:
message = defaultMessage
case .network(let status):
message = status.errorMessage ?? defaultMessage
if let statusCode = status.errorCode {
code = String(statusCode)
}
}
state = .error(message, code)
} else {
state = .loaded
} catch ServiceError.decodeFail {
state = .error(defaultMessage, nil)
} catch let ServiceError.network(status) {
let message = status.errorMessage ?? defaultMessage
var code: String?
if let statusCode = status.errorCode {
code = String(statusCode)
}
state = .error(message, code)
} catch let ServiceError.requestError(error) {
let message = error ?? defaultMessage
state = .error(message, nil)
}

isFetching = false
}
}

Expand Down
16 changes: 9 additions & 7 deletions Projects/Home/Tests/Doubles/HomeServiceProtocolSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,35 @@ import HomeInterfaces

@MainActor
final class HomeServiceProtocolSpy: HomeServiceProtocol, Sendable {
enum Method: Equatable {
enum Method: Equatable, Sendable {
case fetchExchangesList
case fetchDetailsFor
}

var calledMethods: [Method] = []
var fetchExchangesListResult: Result<[ExchangeSummary], Error>?
var fetchDetailsForResult: Result<[ExchangeDetail], Error>?
var fetchExchangesListResult: Result<[ExchangeSummary], ServiceError>?
var fetchDetailsForResult: Result<[ExchangeDetail], ServiceError>?

func fetchExchangesList(page: Int, limit: Int) async throws -> [ExchangeSummary] {
@concurrent
func fetchExchangesList(page: Int, limit: Int) async throws(ServiceError) -> [ExchangeSummary] {
await MainActor.run {
calledMethods.append(.fetchExchangesList)
}

guard let result = fetchExchangesListResult else {
guard let result = await fetchExchangesListResult else {
fatalError("fetchExchangesListResult not set")
}

return try result.get()
}

func fetchDetailsFor(ids: [String]) async throws -> [ExchangeDetail] {
@concurrent
func fetchDetailsFor(ids: [String]) async throws(ServiceError) -> [ExchangeDetail] {
await MainActor.run {
calledMethods.append(.fetchDetailsFor)
}

guard let result = fetchDetailsForResult else {
guard let result = await fetchDetailsForResult else {
fatalError("fetchDetailsForResult not set")
}

Expand Down
14 changes: 9 additions & 5 deletions Projects/Home/Tests/Doubles/NetworkServiceProtocolSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
import Foundation
import NetworkingInterfaces

final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendable {
enum Method: Equatable {
actor NetworkServiceProtocolSpy: NetworkServiceProtocol, Sendable {
enum Method: Equatable, Sendable {
case request
}

var calledMethods: [Method] = []
var requestResult: Result<(Data, HTTPURLResponse), Error>?
private(set) var calledMethods: [Method] = []
private(set) var requestResult: Result<(Data, HTTPURLResponse), NetworkError>?

func request(endpoint: APIEndpointProtocol) async throws -> (Data, HTTPURLResponse) {
func request(endpoint: APIEndpointProtocol) async throws(NetworkError) -> (Data, HTTPURLResponse) {
calledMethods.append(.request)

guard let result = requestResult else {
Expand All @@ -26,4 +26,8 @@ final class NetworkServiceProtocolSpy: NetworkServiceProtocol, @unchecked Sendab

return try result.get()
}

func setResponse(requestResult: Result<(Data, HTTPURLResponse), NetworkError>) async {
self.requestResult = requestResult
}
}
16 changes: 8 additions & 8 deletions Projects/Home/Tests/Service/HomeServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ struct HomeServiceTests {
headerFields: nil
)!

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

let result = try await sut.fetchExchangesList(page: 1, limit: 20)

#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
#expect(result.count == 2)
#expect(result[0].id == 1)
#expect(result[0].name == "Binance")
Expand Down Expand Up @@ -81,12 +81,12 @@ struct HomeServiceTests {
headerFields: nil
)!

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

await #expect(throws: expectedError) {
_ = try await sut.fetchDetailsFor(ids: ["1"])
}
#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
}

@Test("GIVEN successful response WHEN fetchDetailsFor is called THEN returns exchange details")
Expand Down Expand Up @@ -127,11 +127,11 @@ struct HomeServiceTests {
headerFields: nil
)!

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

let result = try await sut.fetchDetailsFor(ids: ["1"])

#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
#expect(result.count == 1)
#expect(result[0].id == 1)
#expect(result[0].name == "Binance")
Expand Down Expand Up @@ -168,12 +168,12 @@ struct HomeServiceTests {
headerFields: nil
)!

networkSpy.requestResult = .success((data, httpResponse))
await networkSpy.setResponse(requestResult: .success((data, httpResponse)))

await #expect(throws: expectedError) {
_ = try await sut.fetchDetailsFor(ids: ["1"])
}
#expect(networkSpy.calledMethods == [.request])
#expect(await networkSpy.calledMethods == [.request])
}
}

Expand Down
Loading
Loading