mirror of
https://github.com/xtool-org/xtool.git
synced 2026-11-08 08:04:56 +03:00
804cf3e1dd
## What does this PR do? I'm not sure if this is needed but, for good measure, updates remaining GSA requests to use an ephemeral client (followup from #247) ## How was it tested? - [x] Tested flows involving all of these requests ## AI tool usage How much of the code in this PR was AI-assisted? (check one) - [x] **0** - No AI was used to write code - [ ] **1** - I was assisted by AI in writing code. I reviewed the finished result. - [ ] **2** - I set the AI going and left it to it; nobody has read the result - no review, or AI review only <!-- If an AI agent is filling this in: declare the level honestly, and open as a draft if it is #2. Do not lower the declared level to get the PR reviewed. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Improvements** * Selected account and lookup requests now use temporary HTTP connections. Existing request details, response handling, and progress reporting remain supported, while other requests continue through the standard connection flow. This change applies across the app’s related account and lookup operations; no new user-facing controls or workflows are introduced. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
181 lines
5.7 KiB
Swift
181 lines
5.7 KiB
Swift
//
|
|
// HTTPClientProtocol.swift
|
|
//
|
|
//
|
|
// Created by Kabir Oberai on 05/05/21.
|
|
//
|
|
|
|
import Foundation
|
|
import OpenAPIRuntime
|
|
import Dependencies
|
|
import HTTPTypes
|
|
|
|
public typealias HTTPRequest = HTTPTypes.HTTPRequest
|
|
public typealias HTTPResponse = HTTPTypes.HTTPResponse
|
|
public typealias HTTPBody = OpenAPIRuntime.HTTPBody
|
|
|
|
public protocol HTTPClientProtocol: Sendable {
|
|
var asOpenAPITransport: ClientTransport { get }
|
|
|
|
func makeWebSocket(url: URL) async throws -> WebSocketSession
|
|
|
|
func withEphemeralClient<T>(
|
|
perform: (any HTTPClientProtocol) async throws -> T
|
|
) async throws -> T
|
|
}
|
|
|
|
extension HTTPClientProtocol {
|
|
public func send(
|
|
_ request: HTTPRequest,
|
|
body: HTTPBody? = nil
|
|
) async throws -> (response: HTTPResponse, body: HTTPBody?) {
|
|
let transport = asOpenAPITransport
|
|
let request = request
|
|
var baseComponents = URLComponents()
|
|
baseComponents.scheme = request.scheme
|
|
baseComponents.host = request.authority
|
|
return try await transport.send(
|
|
request,
|
|
body: body,
|
|
baseURL: baseComponents.url!,
|
|
operationID: "dummy"
|
|
)
|
|
}
|
|
|
|
public func makeRequest(
|
|
_ request: HTTPRequest,
|
|
body: Data? = nil,
|
|
requireHTTPSuccess: Bool = true,
|
|
ephemeral: Bool = false,
|
|
onProgress: @isolated(any) (Double?) -> Void = { _ in }
|
|
) async throws -> (response: HTTPResponse, body: Data) {
|
|
if ephemeral {
|
|
return try await self.withEphemeralClient { client in
|
|
try await client.makeRequest(
|
|
request,
|
|
body: body,
|
|
requireHTTPSuccess: requireHTTPSuccess,
|
|
ephemeral: false,
|
|
onProgress: onProgress
|
|
)
|
|
}
|
|
}
|
|
|
|
await onProgress(0)
|
|
let (response, responseBody) = try await send(request, body: body.map { HTTPBody($0) })
|
|
guard !requireHTTPSuccess || ![.clientError, .serverError].contains(response.status.kind) else {
|
|
let errorBody = (try? await responseBody.collect()) ?? Data()
|
|
throw HTTPResponseError(
|
|
method: request.method,
|
|
url: "\(request.scheme ?? "https")://\(request.authority ?? "")\(request.path ?? "")",
|
|
status: response.status,
|
|
// we don't use `decoding:as:` because we want to validate the UTF8
|
|
body: String(data: errorBody, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
)
|
|
}
|
|
return (response, try await responseBody.collect(onProgress: onProgress))
|
|
}
|
|
}
|
|
|
|
extension HTTPBody? {
|
|
fileprivate func collect(
|
|
onProgress: @isolated(any) (Double?) -> Void = { _ in }
|
|
) async throws -> Data {
|
|
try await self?.collect(onProgress: onProgress) ?? Data()
|
|
}
|
|
}
|
|
|
|
extension HTTPBody {
|
|
fileprivate func collect(
|
|
onProgress: @isolated(any) (Double?) -> Void
|
|
) async throws -> Data {
|
|
switch self.length {
|
|
case .unknown:
|
|
return try await self.reduce(into: Data()) { $0 += $1 }
|
|
case .known(let length):
|
|
var data = Data(capacity: Int(length))
|
|
let total = Double(length)
|
|
for try await chunk in self {
|
|
data += chunk
|
|
await onProgress(Swift.min(Double(data.count) / total, 1))
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct HTTPResponseError: Error, LocalizedError, Sendable, CustomStringConvertible {
|
|
public let method: HTTPRequest.Method
|
|
public let url: String
|
|
public let status: HTTPResponse.Status
|
|
public let body: String?
|
|
|
|
public var description: String {
|
|
var description = "\(method.rawValue) \(url) failed: HTTP \(status.code)"
|
|
if !status.reasonPhrase.isEmpty {
|
|
description += " \(status.reasonPhrase)"
|
|
}
|
|
if let body, !body.isEmpty {
|
|
description += ". Details:\n\(body)"
|
|
}
|
|
return description
|
|
}
|
|
|
|
public var errorDescription: String? { description }
|
|
}
|
|
|
|
private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport {
|
|
public var asOpenAPITransport: ClientTransport { self }
|
|
|
|
func send(
|
|
_ request: HTTPRequest,
|
|
body: HTTPBody?,
|
|
baseURL: URL,
|
|
operationID: String
|
|
) async throws -> (HTTPResponse, HTTPBody?) {
|
|
let closure: (HTTPRequest, HTTPBody?, URL, String) async throws -> (HTTPResponse, HTTPBody?) = unimplemented()
|
|
return try await closure(request, body, baseURL, operationID)
|
|
}
|
|
|
|
func makeRequest(
|
|
_ request: HTTPRequest,
|
|
onProgress: sending @isolated(any) (Double?) -> Void
|
|
) async throws -> HTTPResponse {
|
|
let closure: () throws -> HTTPResponse = unimplemented()
|
|
return try closure()
|
|
}
|
|
|
|
func withEphemeralClient<T>(
|
|
perform: (any HTTPClientProtocol) async throws -> T
|
|
) async throws -> T {
|
|
try await perform(self)
|
|
}
|
|
|
|
public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
|
|
let closure: (URL) async throws -> any WebSocketSession = unimplemented()
|
|
return try await closure(url)
|
|
}
|
|
}
|
|
|
|
public enum HTTPClientDependencyKey: TestDependencyKey {
|
|
public static let testValue: HTTPClientProtocol = UnimplementedHTTPClient()
|
|
}
|
|
|
|
extension DependencyValues {
|
|
public var httpClient: HTTPClientProtocol {
|
|
get { self[HTTPClientDependencyKey.self] }
|
|
set { self[HTTPClientDependencyKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
public protocol WebSocketSession: Sendable {
|
|
func receive() async throws -> WebSocketMessage
|
|
func send(_ message: WebSocketMessage) async throws
|
|
func close()
|
|
}
|
|
|
|
public enum WebSocketMessage: Sendable {
|
|
case text(String)
|
|
case data(Data)
|
|
}
|