-
-
Save djrobby/6cc513563e415ee88c9bf82d853c0061 to your computer and use it in GitHub Desktop.
A simple Swift 5.1 REST client that uses Combine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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