|
| 1 | +// OpenAPITransport.swift |
| 2 | +// |
| 3 | +// Generated by openapi-generator |
| 4 | +// https://openapi-generator.tech |
| 5 | + |
| 6 | +import Foundation |
| 7 | +import Combine |
| 8 | + |
| 9 | +// MARK: - OpenAPITransport |
| 10 | + |
| 11 | +public protocol OpenAPITransport: AnyObject { |
| 12 | + var baseURL: URL? { get } |
| 13 | + |
| 14 | + func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> |
| 15 | + |
| 16 | + func cancelAll() |
| 17 | +} |
| 18 | + |
| 19 | +public struct OpenAPITransportResponse { |
| 20 | + public let data: Data |
| 21 | + public let statusCode: Int |
| 22 | + |
| 23 | + public init(data: Data, statusCode: Int) { |
| 24 | + self.data = data |
| 25 | + self.statusCode = statusCode |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +public struct OpenAPITransportError: Error, CustomStringConvertible, LocalizedError { |
| 30 | + public let statusCode: Int |
| 31 | + public let description: String |
| 32 | + public let errorDescription: String? |
| 33 | + /// It might be source network error |
| 34 | + public let nestedError: Error? |
| 35 | + /// Data may contain additional reason info (like json payload) |
| 36 | + public let data: Data |
| 37 | + |
| 38 | + public init( |
| 39 | + statusCode: Int, |
| 40 | + description: String? = nil, |
| 41 | + errorDescription: String? = nil, |
| 42 | + nestedError: Error? = nil, |
| 43 | + data: Data = Data() |
| 44 | + ) { |
| 45 | + self.statusCode = statusCode |
| 46 | + self.errorDescription = errorDescription |
| 47 | + self.nestedError = nestedError |
| 48 | + self.data = data |
| 49 | + if let description = description { |
| 50 | + self.description = description |
| 51 | + } else { |
| 52 | + var summary = "OpenAPITransportError with status \(statusCode)" |
| 53 | + if let nestedError = nestedError { |
| 54 | + summary.append(contentsOf: ", \(nestedError.localizedDescription)") |
| 55 | + } |
| 56 | + self.description = summary |
| 57 | + } |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +// MARK: - Policy |
| 62 | + |
| 63 | +/// Policy to define whether response is successful or requires authentication |
| 64 | +public protocol ResponsePolicy { |
| 65 | + func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never> |
| 66 | +} |
| 67 | + |
| 68 | +public enum ResponseState { |
| 69 | + /// Return success to client |
| 70 | + case success |
| 71 | + /// Return error to client |
| 72 | + case failure |
| 73 | + /// Repeat request |
| 74 | + case retry |
| 75 | +} |
| 76 | + |
| 77 | +// MARK: - Interceptor |
| 78 | + |
| 79 | +/// Define how to customize URL request before network call |
| 80 | +public protocol Interceptor { |
| 81 | + /// Customize request before performing. Add headers or encrypt body for example. |
| 82 | + func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError> |
| 83 | + |
| 84 | + /// Customize response before handling. Decrypt body for example. |
| 85 | + func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError> |
| 86 | +} |
| 87 | + |
| 88 | +// MARK: - Transport delegate |
| 89 | + |
| 90 | +public protocol OpenAPITransportDelegate: AnyObject { |
| 91 | + func willStart(request: URLRequest) |
| 92 | + |
| 93 | + func didFinish(request: URLRequest, response: HTTPURLResponse?, data: Data) |
| 94 | + |
| 95 | + func didFinish(request: URLRequest, error: Error) |
| 96 | +} |
| 97 | + |
| 98 | +// MARK: - Implementation |
| 99 | + |
| 100 | +open class URLSessionOpenAPITransport: OpenAPITransport { |
| 101 | + public struct Config { |
| 102 | + public var baseURL: URL? |
| 103 | + public var session: URLSession |
| 104 | + public var processor: Interceptor |
| 105 | + public var policy: ResponsePolicy |
| 106 | + public weak var delegate: OpenAPITransportDelegate? |
| 107 | + |
| 108 | + public init( |
| 109 | + baseURL: URL? = nil, |
| 110 | + session: URLSession = .shared, |
| 111 | + processor: Interceptor = DefaultInterceptor(), |
| 112 | + policy: ResponsePolicy = DefaultResponsePolicy(), |
| 113 | + delegate: OpenAPITransportDelegate? = nil |
| 114 | + ) { |
| 115 | + self.baseURL = baseURL |
| 116 | + self.session = session |
| 117 | + self.processor = processor |
| 118 | + self.policy = policy |
| 119 | + self.delegate = delegate |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + private var cancellable = Set<AnyCancellable>() |
| 124 | + public var config: Config |
| 125 | + public var baseURL: URL? { config.baseURL } |
| 126 | + |
| 127 | + public init(config: Config = .init()) { |
| 128 | + self.config = config |
| 129 | + } |
| 130 | + |
| 131 | + open func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> { |
| 132 | + config.processor |
| 133 | + // Add custom headers or refresh token if needed |
| 134 | + .intercept(request: request) |
| 135 | + .flatMap { request -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in |
| 136 | + self.config.delegate?.willStart(request: request) |
| 137 | + // Perform network call |
| 138 | + return self.config.session.dataTaskPublisher(for: request) |
| 139 | + .mapError { |
| 140 | + self.config.delegate?.didFinish(request: request, error: $0) |
| 141 | + return OpenAPITransportError(statusCode: $0.code.rawValue, description: "Network call finished fails") |
| 142 | + } |
| 143 | + .flatMap { output in |
| 144 | + self.config.processor.intercept(output: output) |
| 145 | + } |
| 146 | + .flatMap { output -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in |
| 147 | + let response = output.response as? HTTPURLResponse |
| 148 | + self.config.delegate?.didFinish(request: request, response: response, data: output.data) |
| 149 | + return self.config.policy.defineState(for: request, output: output) |
| 150 | + .setFailureType(to: OpenAPITransportError.self) |
| 151 | + .flatMap { state -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in |
| 152 | + switch state { |
| 153 | + case .success: |
| 154 | + let transportResponse = OpenAPITransportResponse(data: output.data, statusCode: 200) |
| 155 | + return Result.success(transportResponse).publisher.eraseToAnyPublisher() |
| 156 | + case .retry: |
| 157 | + return Fail(error: OpenAPITransportError.retryError).eraseToAnyPublisher() |
| 158 | + case .failure: |
| 159 | + let code = response?.statusCode ?? OpenAPITransportError.noResponseCode |
| 160 | + let transportError = OpenAPITransportError(statusCode: code, data: output.data) |
| 161 | + return Fail(error: transportError).eraseToAnyPublisher() |
| 162 | + } |
| 163 | + }.eraseToAnyPublisher() |
| 164 | + } |
| 165 | + .eraseToAnyPublisher() |
| 166 | + } |
| 167 | + .retry(times: 2) { error -> Bool in |
| 168 | + return error.statusCode == OpenAPITransportError.retryError.statusCode |
| 169 | + }.eraseToAnyPublisher() |
| 170 | + } |
| 171 | + |
| 172 | + open func cancelAll() { |
| 173 | + cancellable.removeAll() |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +public final class DefaultInterceptor: Interceptor { |
| 178 | + public init() {} |
| 179 | + |
| 180 | + public func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError> { |
| 181 | + Just(request) |
| 182 | + .setFailureType(to: OpenAPITransportError.self) |
| 183 | + .eraseToAnyPublisher() |
| 184 | + } |
| 185 | + |
| 186 | + public func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError> { |
| 187 | + Just(output) |
| 188 | + .setFailureType(to: OpenAPITransportError.self) |
| 189 | + .eraseToAnyPublisher() |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +public final class DefaultResponsePolicy: ResponsePolicy { |
| 194 | + public init() {} |
| 195 | + |
| 196 | + public func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never> { |
| 197 | + let state: ResponseState |
| 198 | + switch (output.response as? HTTPURLResponse)?.statusCode { |
| 199 | + case .some(200...299): state = .success |
| 200 | + default: state = .failure |
| 201 | + } |
| 202 | + return Just(state).eraseToAnyPublisher() |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +/// Custom transport errors. It begins with 6.. not to conflict with HTTP codes |
| 207 | +public extension OpenAPITransportError { |
| 208 | + static let incorrectAuthenticationCode = 600 |
| 209 | + static func incorrectAuthenticationError(_ nestedError: Error? = nil) -> OpenAPITransportError { |
| 210 | + OpenAPITransportError( |
| 211 | + statusCode: OpenAPITransportError.incorrectAuthenticationCode, |
| 212 | + description: "Impossible to add authentication headers to request", |
| 213 | + errorDescription: NSLocalizedString( |
| 214 | + "Impossible to add authentication headers to request", |
| 215 | + comment: "Incorrect authentication" |
| 216 | + ), |
| 217 | + nestedError: nestedError |
| 218 | + ) |
| 219 | + } |
| 220 | + |
| 221 | + static let failedAuthenticationRefreshCode = 601 |
| 222 | + static func failedAuthenticationRefreshError(_ nestedError: Error? = nil) -> OpenAPITransportError { |
| 223 | + OpenAPITransportError( |
| 224 | + statusCode: OpenAPITransportError.failedAuthenticationRefreshCode, |
| 225 | + description: "Error while refreshing authentication", |
| 226 | + errorDescription: NSLocalizedString( |
| 227 | + "Error while refreshing authentication", |
| 228 | + comment: "Failed authentication refresh" |
| 229 | + ), |
| 230 | + nestedError: nestedError |
| 231 | + ) |
| 232 | + } |
| 233 | + |
| 234 | + static let noResponseCode = 603 |
| 235 | + static func noResponseError(_ nestedError: Error? = nil) -> OpenAPITransportError { |
| 236 | + OpenAPITransportError( |
| 237 | + statusCode: OpenAPITransportError.noResponseCode, |
| 238 | + description: "There is no HTTP URL response", |
| 239 | + errorDescription: NSLocalizedString( |
| 240 | + "There is no HTTP URL response", |
| 241 | + comment: "No response" |
| 242 | + ), |
| 243 | + nestedError: nestedError |
| 244 | + ) |
| 245 | + } |
| 246 | + |
| 247 | + static let badURLCode = 604 |
| 248 | + static func badURLError(_ nestedError: Error? = nil) -> OpenAPITransportError { |
| 249 | + OpenAPITransportError( |
| 250 | + statusCode: OpenAPITransportError.badURLCode, |
| 251 | + description: "Request URL cannot be created with given parameters", |
| 252 | + errorDescription: NSLocalizedString( |
| 253 | + "Request URL cannot be created with given parameters", |
| 254 | + comment: "Bad URL" |
| 255 | + ), |
| 256 | + nestedError: nestedError |
| 257 | + ) |
| 258 | + } |
| 259 | + |
| 260 | + static let invalidResponseMappingCode = 605 |
| 261 | + static func invalidResponseMappingError(data: Data) -> OpenAPITransportError { |
| 262 | + OpenAPITransportError( |
| 263 | + statusCode: OpenAPITransportError.invalidResponseMappingCode, |
| 264 | + description: "Response data cannot be expected object scheme", |
| 265 | + errorDescription: NSLocalizedString( |
| 266 | + "Response data cannot be expected object scheme", |
| 267 | + comment: "Invalid response mapping" |
| 268 | + ), |
| 269 | + data: data |
| 270 | + ) |
| 271 | + } |
| 272 | + |
| 273 | + static let retryErrorCode = 606 |
| 274 | + static let retryError = OpenAPITransportError(statusCode: OpenAPITransportError.retryErrorCode) |
| 275 | +} |
| 276 | + |
| 277 | +// MARK: - Private |
| 278 | + |
| 279 | +private extension Publishers { |
| 280 | + struct RetryIf<P: Publisher>: Publisher { |
| 281 | + typealias Output = P.Output |
| 282 | + typealias Failure = P.Failure |
| 283 | + |
| 284 | + let publisher: P |
| 285 | + let times: Int |
| 286 | + let condition: (P.Failure) -> Bool |
| 287 | + |
| 288 | + func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { |
| 289 | + guard times > 0 else { return publisher.receive(subscriber: subscriber) } |
| 290 | + |
| 291 | + publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in |
| 292 | + if condition(error) { |
| 293 | + return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher() |
| 294 | + } else { |
| 295 | + return Fail(error: error).eraseToAnyPublisher() |
| 296 | + } |
| 297 | + }.receive(subscriber: subscriber) |
| 298 | + } |
| 299 | + } |
| 300 | +} |
| 301 | + |
| 302 | +private extension Publisher { |
| 303 | + func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> { |
| 304 | + Publishers.RetryIf(publisher: self, times: times, condition: condition) |
| 305 | + } |
| 306 | +} |
0 commit comments