Last active
February 22, 2025 13:41
-
-
Save buh/9c7a5b81c699309e0c28e2338e2eef94 to your computer and use it in GitHub Desktop.
A simple networking API client
This file contains 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
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 } | |
} |
This file contains 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
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 | |
} | |
} |
This file contains 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
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 { | |
// ... | |
} |
This file contains 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
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