Skip to content

Instantly share code, notes, and snippets.

@Tarkhan99
Created October 13, 2022 11:37
Show Gist options
  • Save Tarkhan99/925431a1c614fb0c62a33c71e2430ade to your computer and use it in GitHub Desktop.
Save Tarkhan99/925431a1c614fb0c62a33c71e2430ade to your computer and use it in GitHub Desktop.
import Foundation
import Combine
protocol APIRequestType {
associatedtype ModelType: Decodable
var path: String {get}
var method: String {get}
var headers: [String: String]? {get}
var queryItems: [URLQueryItem]? {get}
func body() throws -> Data?
}
enum APIServiceError: Error {
case invalidURL
case httpError(HTTPCode)
case parseError
case unexpectedResponse
}
extension APIServiceError: LocalizedError {
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case let .httpError(statusCode): return "Unexpected HTTP status code: \(statusCode)"
case .parseError: return "Unexpected JSON parse error"
case .unexpectedResponse: return "Unexpected response from the server"
}
}
}
typealias HTTPCode = Int
typealias HTTPCodes = Range<HTTPCode>
extension HTTPCodes {
static let success = 200 ..< 300
}
extension APIRequestType {
func buildRequest(baseURL: String) throws -> URLRequest {
guard let url = URL(string: baseURL + path) else {
throw APIServiceError.invalidURL
}
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = method
request.allHTTPHeaderFields = headers
request.httpBody = try body()
return request
}
}
struct Request: APIRequestType {
typealias ModelType = Response
var path: String
var method: String { return "GET" }
var headers: [String: String]? { return ["Content-Type": "application/json"] }
var queryItems: [URLQueryItem]?
func body() throws -> Data? {
return Data()
}
}
protocol APIServiceType {
var session: URLSession {get}
var baseURL: String {get}
var bgQueue: DispatchQueue {get}
func call<Request>(from endpoint: Request) -> AnyPublisher<Request.ModelType, Error> where Request: APIRequestType
}
final class APIService: APIServiceType {
internal let baseURL: String
internal let session: URLSession = URLSession.shared
internal let bgQueue: DispatchQueue = DispatchQueue.main
init(baseURL: String = "") {
self.baseURL = baseURL
}
func call<Request>(from endpoint: Request) -> AnyPublisher<Request.ModelType, Error> where Request : APIRequestType {
do {
let request = try endpoint.buildRequest(baseURL: baseURL)
return session.dataTaskPublisher(for: request)
.retry(1)
.tryMap {
guard let code = ($0.response as? HTTPURLResponse)?.statusCode else {
throw APIServiceError.unexpectedResponse
}
guard HTTPCodes.success.contains(code) else {
throw APIServiceError.httpError(code)
}
return $0.data
}
.decode(type: Request.ModelType.self, decoder: JSONDecoder())
.mapError {_ in APIServiceError.parseError}
.receive(on: self.bgQueue)
.eraseToAnyPublisher()
} catch let error {
return Fail<Request.ModelType, Error>(error: error).eraseToAnyPublisher()
}
}
}
let apiService: APIServiceType = ApiService()
let request = Request(path: "/path")
let publisher = apiService.call(from: endpoint)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment