Created
November 18, 2025 04:52
-
-
Save robertvunabandi/b00695d5ea583f528105167b8a675348 to your computer and use it in GitHub Desktop.
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
| // | |
| // 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