Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View 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)
}
}