144 lines
4.7 KiB
Swift
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
|
|
}
|