Skip to content

Instantly share code, notes, and snippets.

@sarpsolakoglu
Created November 14, 2019 17:32
Show Gist options
  • Save sarpsolakoglu/4b29c1d3bb41ec0318f24e6391f2f2cb to your computer and use it in GitHub Desktop.
Save sarpsolakoglu/4b29c1d3bb41ec0318f24e6391f2f2cb to your computer and use it in GitHub Desktop.
A simple Swift 5.1 REST client that uses combine
//
// CombineClient.swift
// CombineClient
//
// Created by Sarp Solakoglu on 14/11/2019.
// Copyright © 2019 Sarp Solakoglu. All rights reserved.
//
import Foundation
import Combine
enum RestMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
enum ClientError: Error {
case network(description: String)
case parsing(description: String)
}
struct EmptyRequest: Encodable {}
struct EmptyResponse: Decodable {}
protocol Client {
var baseURL: URL { get }
var session: URLSession { get }
func get<D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String: String]?, headers: [String: String]?) -> AnyPublisher<D, ClientError>
func post<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String: String]?, body: E?, headers: [String: String]?) -> AnyPublisher<D, ClientError>
func put<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, body: E?, headers: [String: String]?) -> AnyPublisher<D, ClientError>
func delete<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, body: E?, headers: [String: String]?) -> AnyPublisher<D, ClientError>
func performRequest<D: Decodable>(_ responseType: D.Type, request: URLRequest) -> AnyPublisher<D, ClientError>
}
class CombineClient {
let baseURL: URL
let session: URLSession
let defaultHeaders: [String: String]?
init(baseURL: URL, defaultHeaders: [String: String]? = nil) {
self.baseURL = baseURL
self.defaultHeaders = defaultHeaders
self.session = URLSession(configuration: URLSessionConfiguration.default)
}
}
extension CombineClient: Client {
func get<D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String: String]? = nil, headers: [String: String]? = nil) -> AnyPublisher<D, ClientError> {
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params)
let request = self.buildRequest(url: url, method: RestMethod.get.rawValue, headers: headers, body: EmptyRequest())
return self.performRequest(responseType, request: request)
}
func post<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String: String]? = nil, body: E?, headers: [String: String]? = nil) -> AnyPublisher<D, ClientError> {
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params)
let request = self.buildRequest(url: url, method: RestMethod.post.rawValue, headers: headers, body: body)
return self.performRequest(responseType, request: request)
}
func put<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, body: E?, headers: [String: String]? = nil) -> AnyPublisher<D, ClientError> {
let url = baseURL.addEndpoint(endpoint: endpoint)
let request = self.buildRequest(url: url, method: RestMethod.put.rawValue, headers: headers, body: body)
return self.performRequest(responseType, request: request)
}
func delete<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, body: E?, headers: [String: String]? = nil) -> AnyPublisher<D, ClientError> {
let url = baseURL.addEndpoint(endpoint: endpoint)
let request = self.buildRequest(url: url, method: RestMethod.delete.rawValue, headers: headers, body: body)
return self.performRequest(responseType, request: request)
}
func performRequest<D: Decodable>(_ responseType: D.Type, request: URLRequest) -> AnyPublisher<D, ClientError> {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return session.dataTaskPublisher(for: request)
.mapError { error in
.network(description: error.localizedDescription)
}
.flatMap(maxPublishers: .max(1)) { pair in
self.decode(pair.data)
}
.eraseToAnyPublisher()
}
private func decode<D: Decodable>(_ data: Data) -> AnyPublisher<D, ClientError> {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return Just(data)
.decode(type: D.self, decoder: decoder)
.mapError { error in
.parsing(description: error.localizedDescription)
}
.eraseToAnyPublisher()
}
private func buildRequest<E: Encodable>(url: URL, method: String, headers: [String: String]?, body: E?) -> URLRequest {
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = method
if let defaultHeaders = self.defaultHeaders {
for (key, value) in defaultHeaders {
request.addValue(value, forHTTPHeaderField: key)
}
}
if let requestHeaders = headers {
for (key, value) in requestHeaders {
request.addValue(value, forHTTPHeaderField: key)
}
}
if let requestBody = body {
if !(requestBody is EmptyRequest) {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try? encoder.encode(requestBody)
request.httpBody = data
}
}
return request
}
}
fileprivate extension URL {
func addEndpoint(endpoint: String) -> URL {
return URL(string: endpoint, relativeTo: self)!
}
func addParams(params: [String: String]?) -> URL {
guard let params = params else {
return self
}
var urlComp = URLComponents(url: self, resolvingAgainstBaseURL: true)!
var queryItems = [URLQueryItem]()
for (key, value) in params {
queryItems.append(URLQueryItem(name: key, value: value))
}
urlComp.queryItems = queryItems
return urlComp.url!
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment