Skip to content

Commit f8e4dbe

Browse files
authored
tech(session): use async as main implementation (#7)
1 parent 9e6a06c commit f8e4dbe

File tree

13 files changed

+269
-222
lines changed

13 files changed

+269
-222
lines changed

Sources/SimpleHTTP/DataCoder.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ public protocol ContentDataDecoder: DataDecoder {
2121
/// a http content type
2222
static var contentType: HTTPContentType { get }
2323
}
24+
25+
/// A function converting data when a http error occur into a custom error
26+
public typealias DataErrorDecoder = (Data) throws -> Error

Sources/SimpleHTTP/Foundation/Publisher+Validate.swift

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
import Foundation
22

33
extension URLSession {
4-
/// Return a dataTaskPublisher as a `DataPublisher`
5-
public func dataPublisher(for request: URLRequest) -> Session.RequestDataPublisher {
6-
dataTaskPublisher(for: request)
7-
.mapError { $0 as Error }
8-
.eraseToAnyPublisher()
4+
@available(iOS, deprecated: 15.0, message: "Use built-in API instead")
5+
public func data(for urlRequest: URLRequest) async throws -> (Data, URLResponse) {
6+
try await withCheckedThrowingContinuation { promise in
7+
self.dataTask(with: urlRequest) { data, response, error in
8+
if let error = error {
9+
promise.resume(throwing: error)
10+
}
11+
12+
guard let data = data, let response = response else {
13+
return promise.resume(throwing: URLError(.badServerResponse))
14+
}
15+
16+
promise.resume(returning: (data, response))
17+
}
18+
.resume()
19+
}
20+
}
21+
22+
public func data(for urlRequest: URLRequest) async throws -> URLDataResponse {
23+
let (data, response) = try await data(for: urlRequest)
24+
25+
return URLDataResponse(data: data, response: response as! HTTPURLResponse)
926
}
1027
}

Sources/SimpleHTTP/Interceptor/Interceptor.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,32 @@ public protocol ResponseInterceptor {
2525
/// - Parameter request: the request that was sent to the server
2626
func receivedResponse<Output>(_ result: Result<Output, Error>, for request: Request<Output>)
2727
}
28+
29+
extension RequestInterceptor {
30+
func shouldRescueRequest<Output>(_ request: Request<Output>, error: Error) async throws -> Bool {
31+
var cancellable: Set<AnyCancellable> = []
32+
let onCancel = { cancellable.removeAll() }
33+
34+
guard let rescuePublisher = rescueRequest(request, error: error) else {
35+
return false
36+
}
37+
38+
return try await withTaskCancellationHandler(
39+
handler: { onCancel() },
40+
operation: {
41+
try await withCheckedThrowingContinuation { continuation in
42+
rescuePublisher
43+
.sink(
44+
receiveCompletion: {
45+
if case let .failure(error) = $0 {
46+
return continuation.resume(throwing: error)
47+
}
48+
},
49+
receiveValue: { _ in
50+
continuation.resume(returning: true)
51+
})
52+
.store(in: &cancellable)
53+
}
54+
})
55+
}
56+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
public struct URLDataResponse {
4+
public let data: Data
5+
public let response: HTTPURLResponse
6+
}
7+
8+
extension URLDataResponse {
9+
public func validate(errorDecoder: DataErrorDecoder? = nil) throws {
10+
do {
11+
try response.validateStatusCode()
12+
}
13+
catch let error as HTTPError {
14+
guard let decoder = errorDecoder, !data.isEmpty else {
15+
throw error
16+
}
17+
18+
throw try decoder(data)
19+
}
20+
}
21+
}

Sources/SimpleHTTP/Session/Session+Async.swift

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
3+
#if canImport(Combine)
4+
import Combine
5+
6+
extension Session {
7+
/// Return a publisher performing request and returning `Output` data
8+
///
9+
/// The request is validated and decoded appropriately on success.
10+
/// - Returns: a Publisher emitting Output on success, an error otherwise
11+
public func publisher<Output: Decodable>(for request: Request<Output>) -> AnyPublisher<Output, Error> {
12+
let subject = PassthroughSubject<Output, Error>()
13+
14+
Task {
15+
do {
16+
subject.send(try await response(for: request))
17+
subject.send(completion: .finished)
18+
}
19+
catch {
20+
subject.send(completion: .failure(error))
21+
}
22+
}
23+
24+
return subject.eraseToAnyPublisher()
25+
}
26+
27+
public func publisher(for request: Request<Void>) -> AnyPublisher<Void, Error> {
28+
let subject = PassthroughSubject<Void, Error>()
29+
30+
Task {
31+
do {
32+
subject.send(try await response(for: request))
33+
subject.send(completion: .finished)
34+
}
35+
catch {
36+
subject.send(completion: .failure(error))
37+
}
38+
}
39+
40+
return subject.eraseToAnyPublisher()
41+
}
42+
}
43+
44+
#endif

Sources/SimpleHTTP/Session/Session.swift

Lines changed: 55 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,96 @@
11
import Foundation
2-
import Combine
32

4-
/// Primary class of the library used to perform http request using `Request` objects
3+
/// Primary class of the library used to perform http request using a `Request` object
54
public class Session {
6-
/// Data returned by a http request
7-
public typealias RequestData = URLSession.DataTaskPublisher.Output
8-
9-
/// a Publisher emitting `RequestData`
10-
public typealias RequestDataPublisher = AnyPublisher<RequestData, Error>
11-
5+
/// a function returning a `RequestData` from a `URLRequest`
6+
public typealias URLRequestTask = (URLRequest) async throws -> URLDataResponse
7+
128
let baseURL: URL
139
let config: SessionConfiguration
14-
/// a closure returning a publisher based for a given `URLRequest`
15-
let urlRequestPublisher: (URLRequest) -> RequestDataPublisher
16-
10+
/// a closure returning a `DataResponse` from a `URLRequest`
11+
let dataTask: URLRequestTask
12+
1713
/// init the class using a `URLSession` instance
1814
/// - Parameter baseURL: common url for all the requests. Allow to switch environments easily
1915
/// - Parameter configuration: session configuration to use
2016
/// - Parameter urlSession: `URLSession` instance to use to make requests.
21-
public convenience init(baseURL: URL, configuration: SessionConfiguration = .init(), urlSession: URLSession) {
22-
self.init(
23-
baseURL: baseURL,
24-
configuration: configuration,
25-
dataPublisher: urlSession.dataPublisher(for:)
26-
)
17+
public convenience init(
18+
baseURL: URL,
19+
configuration: SessionConfiguration = SessionConfiguration(),
20+
urlSession: URLSession = .shared
21+
) {
22+
self.init(baseURL: baseURL, configuration: configuration, dataTask: { try await urlSession.data(for: $0) })
2723
}
28-
24+
2925
/// init the class with a base url for request
3026
/// - Parameter baseURL: common url for all the requests. Allow to switch environments easily
3127
/// - Parameter configuration: session configuration to use
32-
/// - Parameter dataPublisher: publisher used by the class to make http requests. If none provided it default
28+
/// - Parameter dataTask: publisher used by the class to make http requests. If none provided it default
3329
/// to `URLSession.dataPublisher(for:)`
3430
public init(
3531
baseURL: URL,
3632
configuration: SessionConfiguration = SessionConfiguration(),
37-
dataPublisher: @escaping (URLRequest) -> RequestDataPublisher = { URLSession.shared.dataPublisher(for: $0) }
33+
dataTask: @escaping URLRequestTask
3834
) {
3935
self.baseURL = baseURL
4036
self.config = configuration
41-
self.urlRequestPublisher = dataPublisher
37+
self.dataTask = dataTask
4238
}
43-
44-
/// Return a publisher performing request and returning `Output` data
39+
40+
/// Return a publisher performing `request` and returning `Output`
4541
///
4642
/// The request is validated and decoded appropriately on success.
47-
/// - Returns: a Publisher emitting Output on success, an error otherwise
48-
public func publisher<Output: Decodable>(for request: Request<Output>) -> AnyPublisher<Output, Error> {
49-
dataPublisher(for: request)
50-
.receive(on: config.decodingQueue)
51-
.map { response -> (output: Result<Output, Error>, request: Request<Output>) in
52-
let output = Result {
53-
try self.config.interceptor.adaptOutput(
54-
try self.config.decoder.decode(Output.self, from: response.data),
55-
for: response.request
56-
)
57-
}
58-
59-
return (output: output, request: response.request)
60-
}
61-
.handleEvents(receiveOutput: { self.log($0.output, for: $0.request) })
62-
.tryMap { try $0.output.get() }
63-
.eraseToAnyPublisher()
43+
/// - Returns: a async Output on success, an error otherwise
44+
public func response<Output: Decodable>(for request: Request<Output>) async throws -> Output {
45+
let result = try await dataPublisher(for: request)
46+
47+
do {
48+
let decodedOutput = try config.decoder.decode(Output.self, from: result.data)
49+
let output = try config.interceptor.adaptOutput(decodedOutput, for: result.request)
50+
51+
log(.success(output), for: result.request)
52+
return output
53+
}
54+
catch {
55+
log(.failure(error), for: result.request)
56+
throw error
57+
}
6458
}
65-
66-
/// Return a publisher performing request which has no return value
67-
public func publisher(for request: Request<Void>) -> AnyPublisher<Void, Error> {
68-
dataPublisher(for: request)
69-
.handleEvents(receiveOutput: { self.log(.success(()), for: $0.request) })
70-
.map { _ in () }
71-
.eraseToAnyPublisher()
59+
60+
/// Perform asynchronously `request` which has no return value
61+
public func response(for request: Request<Void>) async throws {
62+
let result = try await dataPublisher(for: request)
63+
log(.success(()), for: result.request)
7264
}
7365
}
7466

7567
extension Session {
76-
private func dataPublisher<Output>(for request: Request<Output>) -> AnyPublisher<Response<Output>, Error> {
68+
private func dataPublisher<Output>(for request: Request<Output>) async throws -> Response<Output> {
7769
let modifiedRequest = config.interceptor.adaptRequest(request)
78-
70+
let urlRequest = try modifiedRequest
71+
.toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder)
72+
7973
do {
80-
let urlRequest = try modifiedRequest
81-
.toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder)
74+
let result = try await dataTask(urlRequest)
8275

83-
return urlRequestPublisher(urlRequest)
84-
.validate(config.errorConverter)
85-
.map { Response(data: $0.data, request: modifiedRequest) }
86-
.tryCatch { try self.rescue(error: $0, request: request) }
87-
.handleEvents(receiveCompletion: { self.logIfFailure($0, for: modifiedRequest) })
88-
.eraseToAnyPublisher()
76+
try result.validate(errorDecoder: config.errorConverter)
77+
78+
return Response(data: result.data, request: modifiedRequest)
8979
}
9080
catch {
91-
return Fail(error: error).eraseToAnyPublisher()
92-
}
93-
}
94-
95-
/// log a request completion
96-
private func logIfFailure<Output>(_ completion: Subscribers.Completion<Error>, for request: Request<Output>) {
97-
if case .failure(let error) = completion {
98-
config.interceptor.receivedResponse(.failure(error), for: request)
81+
self.log(.failure(error), for: modifiedRequest)
82+
83+
if try await config.interceptor.shouldRescueRequest(modifiedRequest, error: error) {
84+
return try await dataPublisher(for: modifiedRequest)
85+
}
86+
87+
throw error
9988
}
10089
}
101-
90+
10291
private func log<Output>(_ response: Result<Output, Error>, for request: Request<Output>) {
10392
config.interceptor.receivedResponse(response, for: request)
10493
}
105-
106-
/// try to rescue an error while making a request and retry it when rescue suceeded
107-
private func rescue<Output>(error: Error, request: Request<Output>) throws -> AnyPublisher<Response<Output>, Error> {
108-
guard let rescue = config.interceptor.rescueRequest(request, error: error) else {
109-
throw error
110-
}
111-
112-
return rescue
113-
.map { self.dataPublisher(for: request) }
114-
.switchToLatest()
115-
.eraseToAnyPublisher()
116-
}
11794
}
11895

11996
private struct Response<Output> {

Sources/SimpleHTTP/Session/SessionConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public struct SessionConfiguration {
1212
/// To apply multiple interceptors use `ComposeInterceptor`
1313
let interceptor: Interceptor
1414
/// a function decoding data (using `decoder`) as a custom error
15-
private(set) var errorConverter: DataErrorConverter?
15+
private(set) var errorConverter: DataErrorDecoder?
1616

1717
/// - Parameter encoder to use for request bodies
1818
/// - Parameter decoder used to decode http responses

0 commit comments

Comments
 (0)