macOS client store and API
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
|
||||
final class API: Sendable {
|
||||
let origin: URL
|
||||
let session: URLSession
|
||||
let client: Client
|
||||
|
||||
init(origin: URL) {
|
||||
self.origin = origin
|
||||
session = URLSession(configuration: .default)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user