Skip to content

Instantly share code, notes, and snippets.

@robertvunabandi
Created November 18, 2025 04:52
Show Gist options
  • Select an option

  • Save robertvunabandi/b00695d5ea583f528105167b8a675348 to your computer and use it in GitHub Desktop.

Select an option

Save robertvunabandi/b00695d5ea583f528105167b8a675348 to your computer and use it in GitHub Desktop.
//
// NetworkResource.swift
// Reading Journal
//
import Foundation
// MARK: Base NetworkResource Protocol
/**
* This, and a lot of the stuff in the `Networking` folder, are inspired from
* https://matteomanferdini.com/network-requests-rest-apis-ios-swift
*
* A ``NetworkResource`` is literally what gets returned from an API endpoint
* call. Therefore, there will have to be a resource per API endpoint, which would
* obviously end up being a pain. This is why at large companies they have a ton
* of code generation around these things.
*/
protocol NetworkResource: CustomStringConvertible {
/// What gets returned when this resource is fetched.
associatedtype Result: Decodable
/// The ``HttpMethod`` for this resource.
var method: HttpMethod { get }
/// The URL path. This is essentially what goes after the `.com` but before
/// the query parameters. For instance, in `example.com/keywords/search?a=b`
/// the path is `/keywords/search`. This method expects the path to begin
/// with the slash.
var path: String { get }
/// Returns the `URL` that will be fetched for this resource.
var url: URL { get }
/// This is the ``URLRequest``. It's implemented in the base classes in here
/// so clients don't need to worry about implementing it (in fact they shouldn't!)
func request() throws -> URLRequest
/// The description of this resource
var description: String { get }
}
extension NetworkResource {
/// Utility method to fetch this resource.
func fetch(debug: NetworkRequestDebugOption = .none) async throws -> Self.Result {
try await Network.Request(self).fetch(debug: debug)
}
/// Implementing `description`
var description: String {
"NetworkResource(\(Self.self) | \(method) | \(path))"
}
}
// MARK: GET Resource
typealias QueryItems = [(String, String)]
extension QueryItems {
static var empty: QueryItems { [] }
}
protocol NetworkGETResource: NetworkResource {
/// `(k,v)` pairs of query items. E.g., `/path?a=b` would mean we
/// return `[("a", "b")]` in this value.
var queryItems: QueryItems { get }
}
extension NetworkGETResource {
var method: HttpMethod { .GET }
var url: URL {
// Network.BASE_URL is the Base URL to the server for this app.
// E.g., "https://reading-journal.com"
var components = URLComponents(string: Network.BASE_URL)!
components.path = path
components.queryItems = queryItems.map {
URLQueryItem(name: $0.0, value: $0.1)
}
return components.url!
}
func request() throws -> URLRequest {
var request = URLRequest(url: url)
request.setCommonsFields(method)
return request
}
}
// MARK: POST Resource
protocol NetworkPOSTResource: NetworkResource {
/// A `POST` request must have an associated body type that is `Codable`
/// This type represents that.
associatedtype Body: Codable
/// The following variable returns the `Body` of this post resource
var body: Body { get }
}
extension NetworkPOSTResource {
var method: HttpMethod { .POST }
var url: URL {
var components = URLComponents(string: Network.BASE_URL)!
components.path = path
return components.url!
}
/// Get the `URLRequest` for this `POST` resource. This can throw
/// a ``NetworkError.bodyParsingError``.
func request() throws -> URLRequest {
var requestBody: Data
do {
requestBody = try JSONEncoder().encode(body)
} catch EncodingError.invalidValue(_, _) {
/// Catching the `JSON` error here. The first value is called `key`,
/// but based on the source code, this is actually whatever is passed
/// in (i.e., the `Body`). Then, the second value is of type
/// ``EncodingError.Context``. Either way, we know there
/// was a problem parsing the body and converting it to `JSON`.
/// In that case, we throw an error of our own.
throw NetworkError.bodyParsingError
}
var request = URLRequest(url: url)
request.setBody(requestBody)
request.setCommonsFields(method)
return request
}
}
// MARK: Various extensions
private extension URLRequest {
mutating func setBody(_ data: Data) {
httpBody = data
setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
}
mutating func setCommonsFields(_ method: HttpMethod) {
httpMethod = method.rawValue
setValue("application/json", forHTTPHeaderField: "Accept")
setValue("application/json", forHTTPHeaderField: "Content-Type")
httpShouldHandleCookies = true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment