swift-combine.OpenAPITransport.mustache Maven / Gradle / Ivy
// OpenAPITransport.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
import Foundation
import Combine
// MARK: - OpenAPITransport
public protocol OpenAPITransport: AnyObject {
var baseURL: URL? { get }
func send(request: URLRequest) -> AnyPublisher
func cancelAll()
}
public struct OpenAPITransportResponse {
public let data: Data
public let statusCode: Int
public init(data: Data, statusCode: Int) {
self.data = data
self.statusCode = statusCode
}
}
public struct OpenAPITransportError: Error, CustomStringConvertible, LocalizedError {
public let statusCode: Int
public let description: String
public let errorDescription: String?
/// It might be source network error
public let nestedError: Error?
/// Data may contain additional reason info (like json payload)
public let data: Data
public init(
statusCode: Int,
description: String? = nil,
errorDescription: String? = nil,
nestedError: Error? = nil,
data: Data = Data()
) {
self.statusCode = statusCode
self.errorDescription = errorDescription
self.nestedError = nestedError
self.data = data
if let description = description {
self.description = description
} else {
var summary = "OpenAPITransportError with status \(statusCode)"
if let nestedError = nestedError {
summary.append(contentsOf: ", \(nestedError.localizedDescription)")
}
self.description = summary
}
}
}
// MARK: - Policy
/// Policy to define whether response is successful or requires authentication
public protocol ResponsePolicy {
func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher
}
public enum ResponseState {
/// Return success to client
case success
/// Return error to client
case failure
/// Repeat request
case retry
}
// MARK: - Interceptor
/// Define how to customize URL request before network call
public protocol Interceptor {
/// Customize request before performing. Add headers or encrypt body for example.
func intercept(request: URLRequest) -> AnyPublisher
/// Customize response before handling. Decrypt body for example.
func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher
}
// MARK: - Transport delegate
public protocol OpenAPITransportDelegate: AnyObject {
func willStart(request: URLRequest)
func didFinish(request: URLRequest, response: HTTPURLResponse?, data: Data)
func didFinish(request: URLRequest, error: Error)
}
// MARK: - Implementation
open class URLSessionOpenAPITransport: OpenAPITransport {
public struct Config {
public var baseURL: URL?
public var session: URLSession
public var processor: Interceptor
public var policy: ResponsePolicy
public weak var delegate: OpenAPITransportDelegate?
public init(
baseURL: URL? = nil,
session: URLSession = .shared,
processor: Interceptor = DefaultInterceptor(),
policy: ResponsePolicy = DefaultResponsePolicy(),
delegate: OpenAPITransportDelegate? = nil
) {
self.baseURL = baseURL
self.session = session
self.processor = processor
self.policy = policy
self.delegate = delegate
}
}
private var cancellable = Set()
public var config: Config
public var baseURL: URL? { config.baseURL }
public init(config: Config = .init()) {
self.config = config
}
open func send(request: URLRequest) -> AnyPublisher {
config.processor
// Add custom headers or refresh token if needed
.intercept(request: request)
.flatMap { request -> AnyPublisher in
self.config.delegate?.willStart(request: request)
// Perform network call
return self.config.session.dataTaskPublisher(for: request)
.mapError {
self.config.delegate?.didFinish(request: request, error: $0)
return OpenAPITransportError(statusCode: $0.code.rawValue, description: "Network call finished fails")
}
.flatMap { output in
self.config.processor.intercept(output: output)
}
.flatMap { output -> AnyPublisher in
let response = output.response as? HTTPURLResponse
self.config.delegate?.didFinish(request: request, response: response, data: output.data)
return self.config.policy.defineState(for: request, output: output)
.setFailureType(to: OpenAPITransportError.self)
.flatMap { state -> AnyPublisher in
switch state {
case .success:
let transportResponse = OpenAPITransportResponse(data: output.data, statusCode: 200)
return Result.success(transportResponse).publisher.eraseToAnyPublisher()
case .retry:
return Fail(error: OpenAPITransportError.retryError).eraseToAnyPublisher()
case .failure:
let code = response?.statusCode ?? OpenAPITransportError.noResponseCode
let transportError = OpenAPITransportError(statusCode: code, data: output.data)
return Fail(error: transportError).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.retry(times: 2) { error -> Bool in
return error.statusCode == OpenAPITransportError.retryError.statusCode
}.eraseToAnyPublisher()
}
open func cancelAll() {
cancellable.removeAll()
}
}
public final class DefaultInterceptor: Interceptor {
public init() {}
public func intercept(request: URLRequest) -> AnyPublisher {
Just(request)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
public func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher {
Just(output)
.setFailureType(to: OpenAPITransportError.self)
.eraseToAnyPublisher()
}
}
public final class DefaultResponsePolicy: ResponsePolicy {
public init() {}
public func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher {
let state: ResponseState
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200...299): state = .success
default: state = .failure
}
return Just(state).eraseToAnyPublisher()
}
}
/// Custom transport errors. It begins with 6.. not to conflict with HTTP codes
public extension OpenAPITransportError {
static let incorrectAuthenticationCode = 600
static func incorrectAuthenticationError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.incorrectAuthenticationCode,
description: "Impossible to add authentication headers to request",
errorDescription: NSLocalizedString(
"Impossible to add authentication headers to request",
comment: "Incorrect authentication"
),
nestedError: nestedError
)
}
static let failedAuthenticationRefreshCode = 601
static func failedAuthenticationRefreshError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.failedAuthenticationRefreshCode,
description: "Error while refreshing authentication",
errorDescription: NSLocalizedString(
"Error while refreshing authentication",
comment: "Failed authentication refresh"
),
nestedError: nestedError
)
}
static let noResponseCode = 603
static func noResponseError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.noResponseCode,
description: "There is no HTTP URL response",
errorDescription: NSLocalizedString(
"There is no HTTP URL response",
comment: "No response"
),
nestedError: nestedError
)
}
static let badURLCode = 604
static func badURLError(_ nestedError: Error? = nil) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.badURLCode,
description: "Request URL cannot be created with given parameters",
errorDescription: NSLocalizedString(
"Request URL cannot be created with given parameters",
comment: "Bad URL"
),
nestedError: nestedError
)
}
static let invalidResponseMappingCode = 605
static func invalidResponseMappingError(data: Data) -> OpenAPITransportError {
OpenAPITransportError(
statusCode: OpenAPITransportError.invalidResponseMappingCode,
description: "Response data cannot be expected object scheme",
errorDescription: NSLocalizedString(
"Response data cannot be expected object scheme",
comment: "Invalid response mapping"
),
data: data
)
}
static let retryErrorCode = 606
static let retryError = OpenAPITransportError(statusCode: OpenAPITransportError.retryErrorCode)
}
// MARK: - Private
private extension Publishers {
struct RetryIf: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher
© 2015 - 2024 Weber Informatics LLC | Privacy Policy