Files

144 lines
4.7 KiB
Swift

import ByreAPI
import Foundation
import HTTPTypes
import OpenAPIRuntime
import OpenAPIURLSession
import os
let log = Logger(subsystem: "run.fc.byre.mac", category: "byre")
typealias User = Components.Schemas.User
typealias Project = Components.Schemas.Project
typealias ByreTask = Components.Schemas.Task
typealias AgentSession = Components.Schemas.AgentSession
typealias InboxItem = Components.Schemas.InboxItem
typealias ByreEvent = Components.Schemas.Event
typealias EventPage = Components.Schemas.EventPage
typealias Focus = Components.Schemas.Focus
typealias TaskTab = Components.Schemas.TaskTab
typealias Snapshot = Components.Schemas.Snapshot
typealias ServerStatus = Components.Schemas.Status
struct ApiError: Error, CustomStringConvertible {
let status: Int
let message: String
var description: String {
message
}
}
struct ByreDates: DateTranscoder {
private static let style = Date.ISO8601FormatStyle(includingFractionalSeconds: true)
func encode(_ date: Date) throws -> String {
date.formatted(Self.style)
}
func decode(_ string: String) throws -> Date {
try Self.style.parse(string)
}
}
func jsonDecoder() -> JSONDecoder {
let d = JSONDecoder()
d.dateDecodingStrategy = .custom { decoder in
try ByreDates().decode(try decoder.singleValueContainer().decode(String.self))
}
return d
}
func decodePayload<T: Decodable>(_ payload: OpenAPIObjectContainer, as type: T.Type) throws -> T {
try jsonDecoder().decode(T.self, from: try JSONEncoder().encode(payload))
}
private struct ErrorMiddleware: ClientMiddleware {
func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
let (response, responseBody) = try await next(request, body, baseURL)
let code = response.status.code
if code < 400 {
return (response, responseBody)
}
var data = Data()
if let responseBody {
data = try await Data(collecting: responseBody, upTo: 1 << 20)
}
if response.headerFields[.contentType]?.hasPrefix("application/json") == true {
throw ApiError(status: code, message: try JSONDecoder().decode(Components.Schemas._Error.self, from: data).message)
}
let text = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
throw ApiError(status: code, message: "\(code) \(HTTPURLResponse.localizedString(forStatusCode: code))\(text.isEmpty ? "" : ": \(text)")")
}
}
func rootError(_ error: any Error) -> any Error {
if let e = error as? ClientError {
return rootError(e.underlyingError)
}
return error
}
func errorText(_ error: any Error) -> String {
let e = rootError(error)
if let api = e as? ApiError {
return api.message
}
if let url = e as? URLError {
return url.localizedDescription
}
return String(describing: e)
}
private let clientID = UUID().uuidString
let macName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
final class API: Sendable {
let origin: URL
let session: URLSession
let client: Client
init(origin: URL) {
self.origin = origin
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Byre-Client": clientID,
"Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String,
"Byre-Host": macName,
]
session = URLSession(configuration: config)
client = Client(
serverURL: origin,
configuration: Configuration(dateTranscoder: ByreDates()),
transport: URLSessionTransport(configuration: .init(session: session)),
middlewares: [ErrorMiddleware()]
)
}
func url(_ path: String, _ query: [URLQueryItem] = [], scheme: String = "https") -> URL {
var c = URLComponents(url: origin, resolvingAgainstBaseURL: false)!
c.scheme = scheme
c.path = path
if !query.isEmpty {
c.queryItems = query
}
return c.url!
}
}
func parseOrigin(_ text: String) -> URL? {
guard let c = URLComponents(string: text.trimmingCharacters(in: .whitespaces)), c.scheme?.lowercased() == "https", let host = c.host, !host.isEmpty, c.path.isEmpty || c.path == "/", c.query == nil, c.fragment == nil, c.user == nil else {
return nil
}
var origin = URLComponents()
origin.scheme = "https"
origin.host = host.lowercased()
origin.port = c.port
return origin.url
}