Last active
June 30, 2020 16:38
-
-
Save sarpsolakoglu/9033a319d5a199b4d702a094770f3afa to your computer and use it in GitHub Desktop.
Simple Swift 4 Rest Client that uses the Codable protocol for JSON conversion
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
// | |
// SimpleClient.swift | |
// | |
// Created by Sarp Solakoglu on 18/09/2017. | |
// Copyright © 2017 Sarp Solakoglu. All rights reserved. | |
// | |
import Foundation | |
enum RestMethod: String { | |
case get = "GET" | |
case post = "POST" | |
case put = "PUT" | |
case delete = "DELETE" | |
} | |
struct EmptyRequest: Encodable {} | |
struct EmptyResponse: Decodable {} | |
protocol SimpleClient { | |
var baseURL: URL { get } | |
var session: URLSession { get } | |
func get<D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) | |
func post<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) | |
func put<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) | |
func delete<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) | |
func performRequest<D: Decodable>(_ responseType: D.Type, request: URLRequest, completion: @escaping (D?, URLResponse?, Error?) -> Void) | |
} | |
class DefaultClient { | |
let baseURL: URL | |
let session: URLSession | |
init(baseURL: URL) { | |
self.baseURL = baseURL | |
self.session = URLSession(configuration: URLSessionConfiguration.default) | |
} | |
} | |
extension DefaultClient: SimpleClient { | |
func get<D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) { | |
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params) | |
let request = self.buildRequest(url: url, method: RestMethod.get.rawValue, headers: headers, body: EmptyRequest()) | |
self.performRequest(responseType, request: request, completion: completion) | |
} | |
func post<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) { | |
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params) | |
let request = self.buildRequest(url: url, method: RestMethod.post.rawValue, headers: headers, body: body) | |
self.performRequest(responseType, request: request, completion: completion) | |
} | |
func put<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) { | |
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params) | |
let request = self.buildRequest(url: url, method: RestMethod.put.rawValue, headers: headers, body: body) | |
self.performRequest(responseType, request: request, completion: completion) | |
} | |
func delete<E: Encodable, D: Decodable>(_ responseType: D.Type, endpoint: String, params: [String : String]?, body: E?, headers: [String : String]?, completion: @escaping (D?, URLResponse?, Error?) -> Void) { | |
let url = baseURL.addEndpoint(endpoint: endpoint).addParams(params: params) | |
let request = self.buildRequest(url: url, method: RestMethod.delete.rawValue, headers: headers, body: body) | |
self.performRequest(responseType, request: request, completion: completion) | |
} | |
func performRequest<D: Decodable>(_ responseType: D.Type, request: URLRequest, completion: @escaping (D?, URLResponse?, Error?) -> Void) { | |
self.session.dataTask(with: request) { (data, response, error) in | |
if error != nil || data == nil { | |
completion(nil, response, error) | |
} else { | |
guard let responseData = data else { | |
completion(nil, response, error) | |
return | |
} | |
guard error == nil else { | |
completion(nil, response, error!) | |
return | |
} | |
let decoder = JSONDecoder() | |
do { | |
let result = try decoder.decode(D.self, from: responseData) | |
completion(result, response, nil) | |
} catch { | |
completion(nil, response, error) | |
} | |
} | |
}.resume() | |
} | |
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 requestHeaders = headers { | |
for (key, value) in requestHeaders { | |
request.addValue(value, forHTTPHeaderField: key) | |
} | |
} | |
if let requestBody = body { | |
if !(requestBody is EmptyRequest) { | |
let encoder = JSONEncoder(); | |
request.httpBody = try? encoder.encode(requestBody) | |
} | |
} | |
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! | |
} | |
} |
Thanks!
Is there not an encapsulation issue here? I can create a class conforming to this protocol with methods like MovieDBAPI.getMovies()
but MovieDBAPI.get(), put, post, session, baseURL and all the other internal details of our object will be available as we can't make the protocol private if we wish to use it and the members of that protocol must match its access control.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I liked your code. Could you show an example request?