Skip to content

Instantly share code, notes, and snippets.

@buh
Last active October 16, 2022 16:28
Show Gist options
  • Save buh/9c7a5b81c699309e0c28e2338e2eef94 to your computer and use it in GitHub Desktop.
Save buh/9c7a5b81c699309e0c28e2338e2eef94 to your computer and use it in GitHub Desktop.
A simple networking API client
import Foundation
/// A client protocol for sending API requests.
protocol APIClientProtocol: AnyObject {
/// Sends an API request.
/// - Parameters:
/// - request: the API request.
/// - completion: a completion with the request result.
func send<T: Decodable>(_ request: APIRequest, _ completion: @escaping (Result<T, APIError>) -> Void)
}
// MARK: API Client
final class APIClient {
let urlSession: URLSession
let completionQueue: DispatchQueue
init(configuration: URLSessionConfiguration = .default, completionQueue: DispatchQueue = .global()) {
self.urlSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
self.completionQueue = completionQueue
}
}
extension APIClient: APIClientProtocol {
func send<T: Decodable>(_ request: APIRequest, _ completion: @escaping (Result<T, APIError>) -> Void) {
let urlRequest = request.urlRequest()
print("🔗 \(request.method.rawValue)", urlRequest)
urlSession.dataTask(with: urlRequest) { [weak self] data, response, error in
if let error = error {
self?.finishRequest(with: .failure(.response(response, error)), completion)
} else {
self?.parseResponse(data, completion)
}
}.resume()
}
private func parseResponse<T: Decodable>(_ data: Data?, _ completion: @escaping (Result<T, APIError>) -> Void) {
guard let data = data, !data.isEmpty else {
if T.self == EmptyData.self {
finishRequest(with: .success(EmptyData() as! T), completion)
} else {
finishRequest(with: .failure(.unexpectedEmptyData), completion)
}
return
}
do {
let value = try JSONDecoder().decode(T.self, from: data)
finishRequest(with: .success(value), completion)
} catch {
finishRequest(with: .failure(.decoding(type: String(describing: T.self), error)), completion)
}
}
private func finishRequest<T: Decodable>(
with result: Result<T, APIError>,
_ completion: @escaping (Result<T, APIError>) -> Void
) {
if case .failure(let error) = result {
print("❌🔗", error)
}
completionQueue.async {
completion(result)
}
}
}
// MARK: - API Error
enum APIError: LocalizedError {
case urlRequest(APIRequest, Error)
case response(URLResponse?, Error)
case decoding(type: String, Error)
case unexpectedEmptyData
var errorDescription: String? {
switch self {
case .urlRequest(let request, let error):
return "API Error on creating URL request: \(request) error: \(error.localizedDescription)"
case .response(let response, let error):
return "API Error statusCode: \(response?.statusCode ?? 0) error: \(error.localizedDescription)"
case .decoding(let type, let error):
return "API Error decoding type: \(type) error: \(error.localizedDescription)"
case .unexpectedEmptyData:
return "API Error unexpected empty data"
}
}
}
// MARK: - Helpers
struct EmptyData: Codable {}
extension URLResponse {
var statusCode: Int? { (self as? HTTPURLResponse)?.statusCode }
}
import Foundation
/// A common HTTP network request.
struct APIRequest {
/// HTTP method, e.g. "GET", "POST".
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
/// HTTP method.
let method: HTTPMethod
/// Resource URL.
let url: URL
/// Body.
let body: Data?
init<T: Encodable>(
method: HTTPMethod = .get,
baseURL: URL,
path: String,
queryItems: [URLQueryItem] = [],
data: T
) throws {
let body = try JSONEncoder().encode(data)
self.init(method: method, baseURL: baseURL, path: path, queryItems: queryItems, body: body)
}
init(
method: HTTPMethod = .get,
baseURL: URL,
path: String,
queryItems: [URLQueryItem] = [],
body: Data? = nil
) {
self.method = method
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)
urlComponents?.path.append(path)
if !queryItems.isEmpty {
urlComponents?.queryItems = queryItems
}
guard let url = urlComponents?.url else {
// Using `preconditionFailure` here will help easier to locate the call point and probably the issue.
preconditionFailure("Constructing URL failed for baseURL: \(baseURL) path: \(path), query: \(queryItems)")
}
self.url = url
self.body = body
}
}
// MARK: - URL Request
extension APIRequest {
/// Returns the `URLRequest` based on the request properties.
func urlRequest() -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.httpBody = body
return urlRequest
}
}
let client = ClientAPI()
client.send(.wordList(pageSize: 50)) { (result: Result<Word, APIError>) in
// ...
}
do {
let word = Word(value: "Hello")
client.send(try .addWord(word)) { (result: Result<EmptyData, APIError>) in
// ...
}
} catch {
// ...
}
struct Word: Codable {
let value: String
}
// MARK: Words Specific Request API
extension APIRequest {
static func wordList(pageSize: Int) -> APIRequest {
APIRequest(
baseURL: .wordsAPIBaseURL,
path: "word",
queryItems: [.init(name: "limit", value: String(pageSize))]
)
}
static func addWord(_ word: Word) throws -> APIRequest {
try APIRequest(
method: .post,
baseURL: .wordsAPIBaseURL,
path: "word",
data: word
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment