Skip to content

Instantly share code, notes, and snippets.

@umurgdk
Last active December 8, 2023 17:33
Show Gist options
  • Save umurgdk/1f3b17b63b44108cf0442ae2db33b394 to your computer and use it in GitHub Desktop.
Save umurgdk/1f3b17b63b44108cf0442ae2db33b394 to your computer and use it in GitHub Desktop.
Poor man's network client
import Cocoa
enum HTTPMethod: String {
case get
case put
case post
case delete
}
protocol Request {
associatedtype Response
var method: HTTPMethod { get }
func url(base: URL?) -> URL
func body(encoder: JSONEncoder) throws -> Data?
func decodeResponse(_ response: HTTPURLResponse, data: Data, decoder: JSONDecoder) throws -> Response
}
extension Request {
func body(encoder: JSONEncoder) throws -> Data? {
return nil
}
}
extension Request where Response: Decodable {
func decodeResponse(_ response: HTTPURLResponse, data: Data, decoder: JSONDecoder) throws -> Response {
try decoder.decode(Response.self, from: data)
}
}
extension Request where Response == Void {
func decodeResponse(_ response: HTTPURLResponse, data: Data, decoder: JSONDecoder) throws -> Response {
return ()
}
}
protocol NetworkClient {
func request<R: Request>(_ request: R) async throws -> R.Response
}
struct Network: NetworkClient {
let session: URLSession
let baseURL: URL?
let decoder: JSONDecoder
let encoder: JSONEncoder
init(
session: URLSession = .shared,
baseURL: URL?,
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder()
) {
self.session = session
self.baseURL = baseURL
self.decoder = decoder
self.encoder = encoder
}
func request<R: Request>(_ request: R) async throws -> R.Response {
let urlRequest = try makeURLRequest(from: request)
let (data, urlResponse) = try await session.data(for: urlRequest)
// TODO: check response status code
guard let httpResponse = urlResponse as? HTTPURLResponse else {
throw URLError(.unknown)
}
return try request.decodeResponse(httpResponse, data: data, decoder: decoder)
}
private func makeURLRequest<R: Request>(from request: R) throws -> URLRequest {
let url = request.url(base: baseURL)
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
urlRequest.httpBody = try request.body(encoder: encoder)
return urlRequest
}
}
// MARK: - Example Usage
struct Todo: Decodable {
let label: String
let isDone: Bool
}
struct GetTodos: Request {
typealias Response = [Todo]
let limit: Int?
let method = HTTPMethod.get
func url(base: URL?) -> URL {
var components = URLComponents(string: "/todos", encodingInvalidCharacters: true)
if let limit {
components?.queryItems = [URLQueryItem(name: "limit", value: String(limit))]
}
return components!.url(relativeTo: base)!
}
}
let network = Network(baseURL: URL(string: "https://api.myservice.com"))
let todos = try await network.request(GetTodos(limit: 1))
@umurgdk
Copy link
Author

umurgdk commented Dec 8, 2023

For empty responses:

struct UpdateTodo: Request {
    typealias Response = Void

    let id: String
    let todo: Todo

    let method: HTTPMethod = .put
    func body(encoder: JSONEncoder) throws -> Data? {
        try encoder.encode(todo)
    }

    func url(base: URL?) -> URL {
        URL(string: "/todos/\(id)", relativeTo: base)!
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment