Skip to content

Commit 5d5282f

Browse files
committed
refactor(functions): migrate FunctionsClient to use new HTTP layer
1 parent 783eea1 commit 5d5282f

File tree

2 files changed

+81
-111
lines changed

2 files changed

+81
-111
lines changed

Sources/Functions/FunctionsClient.swift

Lines changed: 72 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import ConcurrencyExtras
22
import Foundation
33
import HTTPTypes
4+
import HTTPTypesFoundation
5+
import OpenAPIURLSession
46

57
#if canImport(FoundationNetworking)
68
import FoundationNetworking
@@ -31,6 +33,7 @@ public final class FunctionsClient: Sendable {
3133
var headers = HTTPFields()
3234
}
3335

36+
private let client: Client
3437
private let http: any HTTPClientType
3538
private let mutableState = LockIsolated(MutableState())
3639
private let sessionConfiguration: URLSessionConfiguration
@@ -85,6 +88,7 @@ public final class FunctionsClient: Sendable {
8588
headers: headers,
8689
region: region,
8790
http: http,
91+
client: Client(serverURL: url, transport: URLSessionTransport()),
8892
sessionConfiguration: sessionConfiguration
8993
)
9094
}
@@ -94,11 +98,13 @@ public final class FunctionsClient: Sendable {
9498
headers: [String: String],
9599
region: String?,
96100
http: any HTTPClientType,
101+
client: Client,
97102
sessionConfiguration: URLSessionConfiguration = .default
98103
) {
99104
self.url = url
100105
self.region = region
101106
self.http = http
107+
self.client = client
102108
self.sessionConfiguration = sessionConfiguration
103109

104110
mutableState.withValue {
@@ -140,6 +146,39 @@ public final class FunctionsClient: Sendable {
140146
}
141147
}
142148

149+
/// Inokes a functions returns the raw response and body.
150+
/// - Parameters:
151+
/// - functionName: The name of the function to invoke.
152+
/// - options: Options for invoking the function. (Default: empty `FunctionInvokeOptions`)
153+
/// - Returns: The raw response and body.
154+
public func invoke(
155+
_ functionName: String,
156+
options: FunctionInvokeOptions = .init()
157+
) async throws -> (HTTPTypes.HTTPResponse, HTTPBody) {
158+
try await self.invoke(functionName, options: options) { ($0, $1) }
159+
}
160+
161+
/// Invokes a function and decodes the response.
162+
///
163+
/// - Parameters:
164+
/// - functionName: The name of the function to invoke.
165+
/// - options: Options for invoking the function. (Default: empty `FunctionInvokeOptions`)
166+
/// - decode: A closure to decode the response data and `HTTPResponse` into a `Response`
167+
/// object.
168+
/// - Returns: The decoded `Response` object.
169+
public func invoke<Response>(
170+
_ functionName: String,
171+
options: FunctionInvokeOptions = .init(),
172+
decode: (HTTPTypes.HTTPResponse, HTTPBody) async throws -> Response
173+
) async throws -> Response {
174+
let (_, response, body) = try await _invoke(
175+
functionName: functionName,
176+
invokeOptions: options
177+
)
178+
179+
return try await decode(response, body)
180+
}
181+
143182
/// Invokes a function and decodes the response.
144183
///
145184
/// - Parameters:
@@ -148,15 +187,20 @@ public final class FunctionsClient: Sendable {
148187
/// - decode: A closure to decode the response data and HTTPURLResponse into a `Response`
149188
/// object.
150189
/// - Returns: The decoded `Response` object.
190+
@available(*, deprecated, message: "Use `invoke` with HTTPBody instead.")
151191
public func invoke<Response>(
152192
_ functionName: String,
153193
options: FunctionInvokeOptions = .init(),
154194
decode: (Data, HTTPURLResponse) throws -> Response
155195
) async throws -> Response {
156-
let response = try await rawInvoke(
157-
functionName: functionName, invokeOptions: options
196+
let (request, response, body) = try await _invoke(
197+
functionName: functionName,
198+
invokeOptions: options
158199
)
159-
return try decode(response.data, response.underlyingResponse)
200+
201+
let data = try await Data(collecting: body, upTo: .max)
202+
203+
return try decode(data, HTTPURLResponse(httpResponse: response, url: request.url ?? self.url)!)
160204
}
161205

162206
/// Invokes a function and decodes the response as a specific type.
@@ -171,8 +215,9 @@ public final class FunctionsClient: Sendable {
171215
options: FunctionInvokeOptions = .init(),
172216
decoder: JSONDecoder = JSONDecoder()
173217
) async throws -> T {
174-
try await invoke(functionName, options: options) { data, _ in
175-
try decoder.decode(T.self, from: data)
218+
try await invoke(functionName, options: options) { _, body in
219+
let data = try await Data(collecting: body, upTo: .max)
220+
return try decoder.decode(T.self, from: data)
176221
}
177222
}
178223

@@ -185,124 +230,47 @@ public final class FunctionsClient: Sendable {
185230
_ functionName: String,
186231
options: FunctionInvokeOptions = .init()
187232
) async throws {
188-
try await invoke(functionName, options: options) { _, _ in () }
233+
try await invoke(functionName, options: options) { (_, _: HTTPBody) in () }
189234
}
190235

191-
private func rawInvoke(
236+
private func _invoke(
192237
functionName: String,
193238
invokeOptions: FunctionInvokeOptions
194-
) async throws -> Helpers.HTTPResponse {
195-
let request = buildRequest(functionName: functionName, options: invokeOptions)
196-
let response = try await http.send(request)
239+
) async throws -> (HTTPTypes.HTTPRequest, HTTPTypes.HTTPResponse, HTTPBody) {
240+
let (request, requestBody) = buildRequest(functionName: functionName, options: invokeOptions)
241+
let (response, responseBody) = try await client.send(request, body: requestBody)
197242

198-
guard 200..<300 ~= response.statusCode else {
199-
throw FunctionsError.httpError(code: response.statusCode, data: response.data)
243+
guard response.status.kind == .successful else {
244+
let data = try await Data(collecting: responseBody, upTo: .max)
245+
throw FunctionsError.httpError(code: response.status.code, data: data)
200246
}
201247

202-
let isRelayError = response.headers[.xRelayError] == "true"
248+
let isRelayError = response.headerFields[.xRelayError] == "true"
203249
if isRelayError {
204250
throw FunctionsError.relayError
205251
}
206252

207-
return response
208-
}
209-
210-
/// Invokes a function with streamed response.
211-
///
212-
/// Function MUST return a `text/event-stream` content type for this method to work.
213-
///
214-
/// - Parameters:
215-
/// - functionName: The name of the function to invoke.
216-
/// - invokeOptions: Options for invoking the function.
217-
/// - Returns: A stream of Data.
218-
///
219-
/// - Warning: Experimental method.
220-
/// - Note: This method doesn't use the same underlying `URLSession` as the remaining methods in the library.
221-
public func _invokeWithStreamedResponse(
222-
_ functionName: String,
223-
options invokeOptions: FunctionInvokeOptions = .init()
224-
) -> AsyncThrowingStream<Data, any Error> {
225-
let (stream, continuation) = AsyncThrowingStream<Data, any Error>.makeStream()
226-
let delegate = StreamResponseDelegate(continuation: continuation)
227-
228-
let session = URLSession(
229-
configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
230-
231-
let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest
232-
233-
let task = session.dataTask(with: urlRequest)
234-
task.resume()
235-
236-
continuation.onTermination = { _ in
237-
task.cancel()
238-
239-
// Hold a strong reference to delegate until continuation terminates.
240-
_ = delegate
241-
}
242-
243-
return stream
253+
return (request, response, responseBody)
244254
}
245255

246-
private func buildRequest(functionName: String, options: FunctionInvokeOptions)
247-
-> Helpers.HTTPRequest
248-
{
249-
var request = HTTPRequest(
250-
url: url.appendingPathComponent(functionName),
256+
private func buildRequest(
257+
functionName: String,
258+
options: FunctionInvokeOptions
259+
) -> (HTTPTypes.HTTPRequest, HTTPBody?) {
260+
var request = HTTPTypes.HTTPRequest(
251261
method: FunctionInvokeOptions.httpMethod(options.method) ?? .post,
252-
query: options.query,
253-
headers: mutableState.headers.merging(with: options.headers),
254-
body: options.body,
255-
timeoutInterval: FunctionsClient.requestIdleTimeout
262+
url: url.appendingPathComponent(functionName).appendingQueryItems(options.query),
263+
headerFields: mutableState.headers.merging(with: options.headers)
256264
)
257265

258-
if let region = options.region ?? region {
259-
request.headers[.xRegion] = region
260-
}
261-
262-
return request
263-
}
264-
}
265-
266-
final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
267-
let continuation: AsyncThrowingStream<Data, any Error>.Continuation
268-
269-
init(continuation: AsyncThrowingStream<Data, any Error>.Continuation) {
270-
self.continuation = continuation
271-
}
272-
273-
func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
274-
continuation.yield(data)
275-
}
276-
277-
func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?) {
278-
continuation.finish(throwing: error)
279-
}
266+
// TODO: Check how to assign FunctionsClient.requestIdleTimeout
280267

281-
func urlSession(
282-
_: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse,
283-
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
284-
) {
285-
defer {
286-
completionHandler(.allow)
268+
if let region = options.region ?? region {
269+
request.headerFields[.xRegion] = region
287270
}
288271

289-
guard let httpResponse = response as? HTTPURLResponse else {
290-
continuation.finish(throwing: URLError(.badServerResponse))
291-
return
292-
}
272+
let body = options.body.map(HTTPBody.init)
293273

294-
guard 200..<300 ~= httpResponse.statusCode else {
295-
let error = FunctionsError.httpError(
296-
code: httpResponse.statusCode,
297-
data: Data()
298-
)
299-
continuation.finish(throwing: error)
300-
return
301-
}
302-
303-
let isRelayError = httpResponse.value(forHTTPHeaderField: "x-relay-error") == "true"
304-
if isRelayError {
305-
continuation.finish(throwing: FunctionsError.relayError)
306-
}
274+
return (request, body)
307275
}
308276
}

Sources/Helpers/HTTP/Client.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import OpenAPIRuntime
88
#endif
99

1010
/// A client that can send HTTP requests and receive HTTP responses.
11-
struct Client: Sendable {
11+
package struct Client: Sendable {
1212

1313
/// The URL of the server, used as the base URL for requests made by the
1414
/// client.
@@ -21,7 +21,7 @@ struct Client: Sendable {
2121
var middlewares: [any ClientMiddleware]
2222

2323
/// Creates a new client.
24-
init(
24+
package init(
2525
serverURL: URL,
2626
transport: any ClientTransport,
2727
middlewares: [any ClientMiddleware] = []
@@ -38,33 +38,35 @@ struct Client: Sendable {
3838
/// - body: The HTTP request body to send.
3939
/// - Returns: The HTTP response and its body.
4040
/// - Throws: An error if any part of the HTTP operation process fails.
41-
func send(
41+
package func send(
4242
_ request: HTTPTypes.HTTPRequest,
4343
body: HTTPBody? = nil
44-
) async throws -> (HTTPTypes.HTTPResponse, HTTPBody?) {
44+
) async throws -> (HTTPTypes.HTTPResponse, HTTPBody) {
4545
let baseURL = serverURL
4646
var next:
4747
@Sendable (HTTPTypes.HTTPRequest, HTTPBody?, URL) async throws -> (
48-
HTTPTypes.HTTPResponse, HTTPBody?
48+
HTTPTypes.HTTPResponse, HTTPBody
4949
) = {
5050
(_request, _body, _url) in
51-
try await transport.send(
51+
let (response, body) = try await transport.send(
5252
_request,
5353
body: _body,
5454
baseURL: _url,
5555
operationID: ""
5656
)
57+
return (response, body ?? HTTPBody())
5758
}
5859
for middleware in middlewares.reversed() {
5960
let tmp = next
6061
next = { (_request, _body, _url) in
61-
try await middleware.intercept(
62+
let (response, body) = try await middleware.intercept(
6263
_request,
6364
body: _body,
6465
baseURL: _url,
6566
operationID: "",
6667
next: tmp
6768
)
69+
return (response, body ?? HTTPBody())
6870
}
6971
}
7072
return try await next(request, body, baseURL)

0 commit comments

Comments
 (0)