mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
135
Yattee/Services/Networking/Endpoint.swift
Normal file
135
Yattee/Services/Networking/Endpoint.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// Endpoint.swift
|
||||
// Yattee
|
||||
//
|
||||
// Type-safe endpoint protocol for API requests.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// HTTP methods supported by the API.
|
||||
enum HTTPMethod: String, Sendable {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case put = "PUT"
|
||||
case patch = "PATCH"
|
||||
case delete = "DELETE"
|
||||
}
|
||||
|
||||
/// Protocol defining an API endpoint.
|
||||
protocol Endpoint: Sendable {
|
||||
/// The path component of the URL (e.g., "/api/v1/videos").
|
||||
var path: String { get }
|
||||
|
||||
/// The HTTP method for this endpoint.
|
||||
var method: HTTPMethod { get }
|
||||
|
||||
/// Query parameters to append to the URL.
|
||||
var queryItems: [URLQueryItem]? { get }
|
||||
|
||||
/// HTTP headers to include in the request.
|
||||
var headers: [String: String]? { get }
|
||||
|
||||
/// The body data for POST/PUT/PATCH requests.
|
||||
var body: Data? { get }
|
||||
|
||||
/// Timeout interval for this specific request.
|
||||
var timeout: TimeInterval { get }
|
||||
}
|
||||
|
||||
// MARK: - Default Implementations
|
||||
|
||||
extension Endpoint {
|
||||
var method: HTTPMethod { .get }
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
var headers: [String: String]? { nil }
|
||||
var body: Data? { nil }
|
||||
var timeout: TimeInterval { 30 }
|
||||
|
||||
/// Constructs a URLRequest from this endpoint and a base URL.
|
||||
func urlRequest(baseURL: URL) throws -> URLRequest {
|
||||
var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)
|
||||
components?.queryItems = queryItems?.isEmpty == false ? queryItems : nil
|
||||
|
||||
guard let url = components?.url else {
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.timeoutInterval = timeout
|
||||
|
||||
// Set default headers
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// Set custom headers
|
||||
headers?.forEach { key, value in
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// Set body for non-GET requests
|
||||
if let body, method != .get {
|
||||
request.httpBody = body
|
||||
if request.value(forHTTPHeaderField: "Content-Type") == nil {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Generic Endpoints
|
||||
|
||||
/// A generic endpoint that can be configured inline.
|
||||
struct GenericEndpoint: Endpoint, Sendable {
|
||||
let path: String
|
||||
let method: HTTPMethod
|
||||
let queryItems: [URLQueryItem]?
|
||||
let headers: [String: String]?
|
||||
let body: Data?
|
||||
let timeout: TimeInterval
|
||||
|
||||
init(
|
||||
path: String,
|
||||
method: HTTPMethod = .get,
|
||||
queryItems: [URLQueryItem]? = nil,
|
||||
headers: [String: String]? = nil,
|
||||
body: Data? = nil,
|
||||
timeout: TimeInterval = 30
|
||||
) {
|
||||
self.path = path
|
||||
self.method = method
|
||||
self.queryItems = queryItems
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.timeout = timeout
|
||||
}
|
||||
|
||||
/// Creates a GET endpoint with query parameters.
|
||||
nonisolated static func get(_ path: String, query: [String: String] = [:]) -> GenericEndpoint {
|
||||
let queryItems = query.isEmpty ? nil : query.map { URLQueryItem(name: $0.key, value: $0.value) }
|
||||
return GenericEndpoint(path: path, queryItems: queryItems)
|
||||
}
|
||||
|
||||
/// Creates a GET endpoint with custom headers.
|
||||
nonisolated static func get(_ path: String, customHeaders: [String: String]) -> GenericEndpoint {
|
||||
return GenericEndpoint(path: path, headers: customHeaders)
|
||||
}
|
||||
|
||||
/// Creates a POST endpoint with an encodable body.
|
||||
nonisolated static func post<T: Encodable>(_ path: String, body: T, encoder: JSONEncoder = JSONEncoder()) -> GenericEndpoint {
|
||||
let bodyData = try? encoder.encode(body)
|
||||
return GenericEndpoint(path: path, method: .post, body: bodyData)
|
||||
}
|
||||
|
||||
/// Creates a POST endpoint without a body (e.g., for subscription endpoints).
|
||||
nonisolated static func post(_ path: String) -> GenericEndpoint {
|
||||
return GenericEndpoint(path: path, method: .post)
|
||||
}
|
||||
|
||||
/// Creates a DELETE endpoint.
|
||||
nonisolated static func delete(_ path: String) -> GenericEndpoint {
|
||||
return GenericEndpoint(path: path, method: .delete)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user