Skip to content

Commit

Permalink
Resource with generic Error as default
Browse files Browse the repository at this point in the history
  • Loading branch information
lightsprint09 committed Oct 13, 2023
1 parent a0e2245 commit e1f84a3
Show file tree
Hide file tree
Showing 22 changed files with 87 additions and 103 deletions.
17 changes: 11 additions & 6 deletions Source/NetworkService+ResourceWithError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ extension NetworkService {
*/
@discardableResult
public func requestResultWithResponse<Success, E: Error>(
for resource: ResourceWithError<Success, E>
for resource: Resource<Success, E>
) async -> Result<(Success, HTTPURLResponse), E> {
let resourceWithoutError = Resource(request: resource.request, parse: resource.parse)
let resourceWithoutError = Resource<Success, NetworkError>(request: resource.request, parse: resource.parse)
return await self.requestResultWithResponse(for: resourceWithoutError)
.mapError(resource.mapError)
}
Expand Down Expand Up @@ -85,9 +85,9 @@ extension NetworkService {
*/
@discardableResult
public func requestResult<Success, E: Error>(
for resource: ResourceWithError<Success, E>
for resource: Resource<Success, E>
) async -> Result<Success, E> {
let resourceWithoutError = Resource(request: resource.request, parse: resource.parse)
let resourceWithoutError = Resource<Success, NetworkError>(request: resource.request, parse: resource.parse)
return await requestResultWithResponse(for: resourceWithoutError)
.mapError(resource.mapError)
.map({ $0.0 })
Expand Down Expand Up @@ -120,13 +120,18 @@ extension NetworkService {
*/
@discardableResult
public func request<Success, E: Error>(
_ resource: ResourceWithError<Success, E>
_ resource: Resource<Success, E>
) async throws -> Success {
let resourceWithoutError = Resource(request: resource.request, parse: resource.parse)
let resourceWithoutError = Resource<Success, NetworkError>(request: resource.request, parse: resource.parse)
return try await requestResultWithResponse(for: resourceWithoutError)
.mapError(resource.mapError)
.map({ $0.0 })
.get()
}

@discardableResult
func requestWithResponse<Success, E: Error>(for resource: Resource<Success, E>) async throws -> (Success, HTTPURLResponse) {
return try await requestResultWithResponse(for: resource).get()
}

}
11 changes: 8 additions & 3 deletions Source/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public protocol NetworkService: Sendable {
- returns: a running network task
*/
@discardableResult
func requestResultWithResponse<Success>(for resource: Resource<Success>) async -> Result<(Success, HTTPURLResponse), NetworkError>
func requestResultWithResponse<Success>(for resource: Resource<Success, NetworkError>) async -> Result<(Success, HTTPURLResponse), NetworkError>
}

public extension NetworkService {
Expand Down Expand Up @@ -87,7 +87,7 @@ public extension NetworkService {
- returns: a running network task
*/
@discardableResult
func requestResult<Success>(for resource: Resource<Success>) async -> Result<Success, NetworkError> {
func requestResult<Success>(for resource: Resource<Success, NetworkError>) async -> Result<Success, NetworkError> {
return await requestResultWithResponse(for: resource).map({ $0.0 })
}

Expand Down Expand Up @@ -116,7 +116,12 @@ public extension NetworkService {
- returns: a running network task
*/
@discardableResult
func request<Success>(_ resource: Resource<Success>) async throws -> Success {
func request<Success>(_ resource: Resource<Success, NetworkError>) async throws -> Success {
return try await requestResultWithResponse(for: resource).get().0
}

@discardableResult
func requestWithResponse<Success>(for resource: Resource<Success, NetworkError>) async throws -> (Success, HTTPURLResponse) {
return try await requestResultWithResponse(for: resource).get()
}
}
2 changes: 1 addition & 1 deletion Source/NetworkServices/BasicNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public final class BasicNetworkService: NetworkService {
- returns: a running network task
*/
@discardableResult
public func requestResultWithResponse<Success>(for resource: Resource<Success>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
public func requestResultWithResponse<Success>(for resource: Resource<Success, NetworkError>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
do {
let (data, response) = try await networkAccess.load(request: resource.request)
guard let response = response as? HTTPURLResponse else {
Expand Down
4 changes: 2 additions & 2 deletions Source/NetworkServices/ModifyRequestNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ public final class ModifyRequestNetworkService: NetworkService {
- returns: a running network task
*/
@discardableResult
public func requestResultWithResponse<Success>(for resource: Resource<Success>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
public func requestResultWithResponse<Success>(for resource: Resource<Success, NetworkError>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
let request = requestModifications.reduce(resource.request, { request, modify in
return modify(request)
})
let newResource = Resource(request: request, parse: resource.parse)
let newResource = Resource<Success, NetworkError>(request: request, parse: resource.parse)
return await networkService.requestResultWithResponse(for: newResource)
}
}
2 changes: 1 addition & 1 deletion Source/NetworkServices/NetworkServiceMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public final actor NetworkServiceMock: NetworkService {
- parameter onError: Callback which gets called when fetching or transforming fails.

*/
public func requestResultWithResponse<Success>(for resource: Resource<Success>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
public func requestResultWithResponse<Success>(for resource: Resource<Success, NetworkError>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
lastRequests.append(resource.request)
if !responses.isEmpty {
let scheduled = responses.removeFirst()
Expand Down
24 changes: 14 additions & 10 deletions Source/NetworkServices/RetryNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public final class RetryNetworkService: NetworkService {

- returns: a running network task
*/
public func requestResultWithResponse<Success>(for resource: Resource<Success>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
public func requestResultWithResponse<Success>(for resource: Resource<Success, NetworkError>) async -> Result<(Success, HTTPURLResponse), NetworkError> {
let result = await networkService.requestResultWithResponse(for: resource)
switch result {
case .success:
Expand All @@ -94,20 +94,24 @@ public final class RetryNetworkService: NetworkService {
private func requestResultWithResponseOnError<Success>(
error: NetworkError,
numberOfRetriesLeft: Int,
resource: Resource<Success>
resource: Resource<Success, NetworkError>
) async -> Result<(Success, HTTPURLResponse), NetworkError> {
if self.shouldRetry(error), numberOfRetriesLeft > 0 {
let duration = UInt64(idleTimeInterval * 1_000_000_000)
try? await Task.sleep(nanoseconds: duration)
#warning("check for cancellation")

let result = await networkService.requestResultWithResponse(for: resource)
switch result {
case .success:
return result
case .failure(let failure):
return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetriesLeft - 1, resource: resource)
do {
try Task.checkCancellation()
let result = await networkService.requestResultWithResponse(for: resource)
switch result {
case .success:
return result
case .failure(let failure):
return await requestResultWithResponseOnError(error: failure, numberOfRetriesLeft: numberOfRetriesLeft - 1, resource: resource)
}
} catch {
return .failure(.cancelled)
}

} else {
return .failure(error)
}
Expand Down
11 changes: 6 additions & 5 deletions Source/Resource+Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ extension Resource where Model: Decodable {
/// - Parameters:
/// - request: The request to get the remote data payload
/// - decoder: a decoder which can decode the payload into the model type
public init(request: URLRequest, decoder: JSONDecoder) {
self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) })
/// - mapError: a closure which maps to Error
public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError)
}
}

extension ResourceWithError where Model: Decodable {
extension Resource where Model: Decodable, E: NetworkErrorConvertible {

/// Creates an instace of Resource where the result type is `Decodable` and
/// can be decoded with the given decoder
Expand All @@ -45,7 +46,7 @@ extension ResourceWithError where Model: Decodable {
/// - request: The request to get the remote data payload
/// - decoder: a decoder which can decode the payload into the model type
/// - mapError: a closure which maps to Error
public init(request: URLRequest, decoder: JSONDecoder, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: mapError)
public init(request: URLRequest, decoder: JSONDecoder) {
self.init(request: request, parse: { try decoder.decode(Model.self, from: $0) }, mapError: { E(networkError: $0) })
}
}
9 changes: 5 additions & 4 deletions Source/Resource+Inspect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension Resource {
This lets one inspect the data payload before data gets parsed.

```swift
let resource: Resource<Train> = //
let resource: Resource<Train, NetworkError> = //
resource.inspectData { data in
print(String(bytes: data, encoding: .utf8))
}
Expand All @@ -36,11 +36,12 @@ extension Resource {
- parameter inspector: closure which gets passed the data
- returns: a new resource which gets instepcted before parsing
*/
public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource<Model> {
return Resource(request: request, parse: { data in
public func inspectData(_ inspector: @escaping (Data) -> Void) -> Resource<Model, E> {
let parse: (Data) throws -> Model = { data in
inspector(data)
return try self.parse(data)
})
}
return Resource<Model, E>(request: request, parse: parse, mapError: mapError)
}

}
23 changes: 3 additions & 20 deletions Source/Resource+Map.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,10 @@ extension Resource {
///
/// - Parameter transform: transforms the original result of the resource
/// - Returns: the transformed resource
public func map<T>(transform: @escaping (Model) throws -> T) -> Resource<T> {
return Resource<T>(request: request, parse: { data in
return try transform(try self.parse(data))
})
}
}

extension ResourceWithError {

/// Maps a resource result to a different resource. This is useful when you have result of R which contains T and your API request a resource of T,
///
/// Error parsing is not changed
///
/// - Parameter transform: transforms the original result of the resource
/// - Returns: the transformed resource
public func map<T>(transform: @escaping (Model) throws -> T) -> ResourceWithError<T, E> {
return ResourceWithError<T, E>(
public func map<T>(transform: @escaping (Model) throws -> T) -> Resource<T, E> {
return Resource<T, E>(
request: request,
parse: { data in
return try transform(try self.parse(data))
},
parse: { return try transform(try self.parse($0)) },
mapError: mapError
)
}
Expand Down
13 changes: 7 additions & 6 deletions Source/Resource+Void.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ public extension Resource where Model == Void {
///
/// - Parameters:
/// - request: The request to get the remote data payload
init(request: URLRequest) {
self.init(request: request, parse: { _ in })
/// - mapError: a closure which maps to Error
init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.init(request: request, parse: { _ in }, mapError: mapError)
}
}

extension ResourceWithError where Model == Void {
public extension Resource where Model == Void, E: NetworkErrorConvertible {

/// Creates an instace of Resource where the result type is `Void`
///
/// - Parameters:
/// - request: The request to get the remote data payload
/// - mapError: a closure which maps to Error
public init(request: URLRequest, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.init(request: request, parse: { _ in }, mapError: mapError)
init(request: URLRequest) {
self.init(request: request, parse: { _ in }, mapError: { E(networkError: $0) })
}

}
19 changes: 11 additions & 8 deletions Source/Resource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,28 @@ import Foundation
**Example**:
```swift
let request: URLRequest = //
let resource: Resource<String?> = Resource(request: request, parse: { data in
let resource: Resource<String?, NetworkError> = Resource(request: request, parse: { data in
String(data: data, encoding: .utf8)
})
```
*/
public struct Resource<Model> {
public struct Resource<Model, E: Error> {
/// The request to fetch the resource remote payload
public let request: URLRequest

/// Parses data into given model.
public let parse: (_ data: Data) throws -> Model

/// Creates a type safe resource, which can be used to fetch it with `NetworkService`
public let mapError: (_ networkError: NetworkError) -> E

/// Creates a type safe resource, which can be used to fetch it with NetworkService
///
/// - Parameters:
/// - request: The request to get the remote data payload
/// - parse: Parses data fetched with the request into given Model
public init(request: URLRequest, parse: @escaping (Data) throws -> Model) {
/// - request: The request to get the remote data payload
/// - parse: Parses data fetched with the request into given Model

public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.request = request
self.parse = parse
self.mapError = mapError
}
}
2 changes: 1 addition & 1 deletion Source/ResourceWithError+NetworkErrorConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public extension ResourceWithError where E: NetworkErrorConvertible {
public extension Resource where E: NetworkErrorConvertible {

init(request: URLRequest, parse: @escaping (Data) throws -> Model) {
self.request = request
Expand Down
21 changes: 1 addition & 20 deletions Source/ResourceWithError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,4 @@ import Foundation
})
```
*/
public struct ResourceWithError<Model, E: Error> {
/// The request to fetch the resource remote payload
public let request: URLRequest

/// Parses data into given model.
public let parse: (_ data: Data) throws -> Model
public let mapError: (_ networkError: NetworkError) -> E

/// Creates a type safe resource, which can be used to fetch it with NetworkService
///
/// - Parameters:
/// - request: The request to get the remote data payload
/// - parse: Parses data fetched with the request into given Model

public init(request: URLRequest, parse: @escaping (Data) throws -> Model, mapError: @escaping (_ networkError: NetworkError) -> E) {
self.request = request
self.parse = parse
self.mapError = mapError
}
}

4 changes: 2 additions & 2 deletions Tests/DecodableResoureTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import XCTest
@testable import DBNetworkStack

class DecodableResoureTest: XCTestCase {
var resource: Resource<Train> {
var resource: Resource<Train, NetworkError> {
let request = URLRequest(path: "/train", baseURL: .defaultMock)
return Resource<Train>(request: request, decoder: JSONDecoder())
return Resource<Train, NetworkError>(request: request, decoder: JSONDecoder())
}

func testResource_withValidData() {
Expand Down
2 changes: 1 addition & 1 deletion Tests/ModifyRequestNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ModifyRequestNetworkServiceTest: XCTestCase {
]
let networkService = ModifyRequestNetworkService(networkService: networkServiceMock, requestModifications: modification)
let request = URLRequest(path: "/trains", baseURL: .defaultMock)
let resource = Resource<Int>(request: request, parse: { _ in return 1 })
let resource = Resource<Int, NetworkError>(request: request, parse: { _ in return 1 })

//When
await networkService.requestResult(for: resource)
Expand Down
2 changes: 1 addition & 1 deletion Tests/NetworkServiceMockTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import DBNetworkStack
class NetworkServiceMockTest: XCTestCase {


let resource: Resource<Train> = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder())
let resource: Resource<Train, NetworkError> = Resource(request: URLRequest(path: "train", baseURL: .defaultMock), decoder: JSONDecoder())

func testRequestCount() async throws {
//Given
Expand Down
2 changes: 1 addition & 1 deletion Tests/NetworkServiceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class NetworkServiceTest: XCTestCase {

let trainName = "ICE"

var resource: Resource<Train> {
var resource: Resource<Train, NetworkError> {
let request = URLRequest(path: "train", baseURL: .defaultMock)
return Resource(request: request, decoder: JSONDecoder())
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/NetworkServiceWithErrorTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ class NetworkServiceWithErrorTest: XCTestCase {

let trainName = "ICE"

var resource: ResourceWithError<Train, CustomError> {
var resource: Resource<Train, CustomError> {
let request = URLRequest(path: "train", baseURL: .defaultMock)
return ResourceWithError(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) })
return Resource(request: request, decoder: JSONDecoder(), mapError: { CustomError(networkError: $0) })
}


Expand Down
2 changes: 1 addition & 1 deletion Tests/ResourceInspectTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class ResourceInspectTest: XCTestCase {
let data = Data()
var capuredParsingData: Data?
var capturedInspectedData: Data?
let resource = Resource<Int>(request: URLRequest.defaultMock, parse: { data in
let resource = Resource<Int, NetworkError>(request: URLRequest.defaultMock, parse: { data in
capuredParsingData = data
return 1
})
Expand Down
Loading

0 comments on commit e1f84a3

Please sign in to comment.