|
1 | 1 | import Foundation |
2 | | -import Combine |
3 | 2 |
|
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 |
5 | 4 | 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 | + |
12 | 8 | let baseURL: URL |
13 | 9 | 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 | + |
17 | 13 | /// init the class using a `URLSession` instance |
18 | 14 | /// - Parameter baseURL: common url for all the requests. Allow to switch environments easily |
19 | 15 | /// - Parameter configuration: session configuration to use |
20 | 16 | /// - 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) }) |
27 | 23 | } |
28 | | - |
| 24 | + |
29 | 25 | /// init the class with a base url for request |
30 | 26 | /// - Parameter baseURL: common url for all the requests. Allow to switch environments easily |
31 | 27 | /// - 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 |
33 | 29 | /// to `URLSession.dataPublisher(for:)` |
34 | 30 | public init( |
35 | 31 | baseURL: URL, |
36 | 32 | configuration: SessionConfiguration = SessionConfiguration(), |
37 | | - dataPublisher: @escaping (URLRequest) -> RequestDataPublisher = { URLSession.shared.dataPublisher(for: $0) } |
| 33 | + dataTask: @escaping URLRequestTask |
38 | 34 | ) { |
39 | 35 | self.baseURL = baseURL |
40 | 36 | self.config = configuration |
41 | | - self.urlRequestPublisher = dataPublisher |
| 37 | + self.dataTask = dataTask |
42 | 38 | } |
43 | | - |
44 | | - /// Return a publisher performing request and returning `Output` data |
| 39 | + |
| 40 | + /// Return a publisher performing `request` and returning `Output` |
45 | 41 | /// |
46 | 42 | /// 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 | + } |
64 | 58 | } |
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) |
72 | 64 | } |
73 | 65 | } |
74 | 66 |
|
75 | 67 | 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> { |
77 | 69 | let modifiedRequest = config.interceptor.adaptRequest(request) |
78 | | - |
| 70 | + let urlRequest = try modifiedRequest |
| 71 | + .toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder) |
| 72 | + |
79 | 73 | do { |
80 | | - let urlRequest = try modifiedRequest |
81 | | - .toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder) |
| 74 | + let result = try await dataTask(urlRequest) |
82 | 75 |
|
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) |
89 | 79 | } |
90 | 80 | 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 |
99 | 88 | } |
100 | 89 | } |
101 | | - |
| 90 | + |
102 | 91 | private func log<Output>(_ response: Result<Output, Error>, for request: Request<Output>) { |
103 | 92 | config.interceptor.receivedResponse(response, for: request) |
104 | 93 | } |
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 | | - } |
117 | 94 | } |
118 | 95 |
|
119 | 96 | private struct Response<Output> { |
|
0 commit comments