• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 12906979978

22 Jan 2025 11:13AM UTC coverage: 54.597% (+6.3%) from 48.322%
12906979978

Pull #645

github

web-flow
Merge 208a0c262 into 37a32aef8
Pull Request #645: test: integration tests revamp

14 of 117 new or added lines in 13 files covered. (11.97%)

46 existing lines in 5 files now uncovered.

3658 of 6700 relevant lines covered (54.6%)

11.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.49
/Sources/Functions/FunctionsClient.swift
1
import ConcurrencyExtras
2
import Foundation
3
import HTTPTypes
4
import Helpers
5

6
#if canImport(FoundationNetworking)
7
  import FoundationNetworking
8
#endif
9

10
let version = Helpers.version
11

12
/// An actor representing a client for invoking functions.
13
public final class FunctionsClient: Sendable {
14
  /// Fetch handler used to make requests.
15
  public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> (
16
    Data, URLResponse
17
  )
18

19
  /// The base URL for the functions.
20
  let url: URL
21

22
  /// The Region to invoke the functions in.
23
  let region: String?
24

25
  struct MutableState {
26
    /// Headers to be included in the requests.
27
    var headers = HTTPFields()
17✔
28
  }
29

30
  private let http: any HTTPClientType
31
  private let mutableState = LockIsolated(MutableState())
17✔
32
  private let sessionConfiguration: URLSessionConfiguration
33

34
  var headers: HTTPFields {
3✔
35
    mutableState.headers
3✔
36
  }
3✔
37

38
  /// Initializes a new instance of `FunctionsClient`.
39
  ///
40
  /// - Parameters:
41
  ///   - url: The base URL for the functions.
42
  ///   - headers: Headers to be included in the requests. (Default: empty dictionary)
43
  ///   - region: The Region to invoke the functions in.
44
  ///   - logger: SupabaseLogger instance to use.
45
  ///   - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
46
  ///   - sessionConfiguration: The `URLSessionConfiguration` used for making requests.
47
  @_disfavoredOverload
48
  public convenience init(
49
    url: URL,
50
    headers: [String: String] = [:],
51
    region: String? = nil,
52
    logger: (any SupabaseLogger)? = nil,
NEW
53
    fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) },
×
54
    sessionConfiguration: URLSessionConfiguration = .default
UNCOV
55
  ) {
×
UNCOV
56
    var interceptors: [any HTTPClientInterceptor] = []
×
UNCOV
57
    if let logger {
×
UNCOV
58
      interceptors.append(LoggerInterceptor(logger: logger))
×
UNCOV
59
    }
×
UNCOV
60

×
UNCOV
61
    let http = HTTPClient(fetch: fetch, interceptors: interceptors)
×
UNCOV
62

×
NEW
63
    self.init(
×
NEW
64
      url: url,
×
NEW
65
      headers: headers,
×
NEW
66
      region: region,
×
NEW
67
      http: http,
×
NEW
68
      sessionConfiguration: sessionConfiguration
×
NEW
69
    )
×
UNCOV
70
  }
×
71

72
  init(
73
    url: URL,
74
    headers: [String: String],
75
    region: String?,
76
    http: any HTTPClientType,
77
    sessionConfiguration: URLSessionConfiguration = .default
UNCOV
78
  ) {
×
UNCOV
79
    self.url = url
×
UNCOV
80
    self.region = region
×
UNCOV
81
    self.http = http
×
NEW
82
    self.sessionConfiguration = sessionConfiguration
×
UNCOV
83

×
UNCOV
84
    mutableState.withValue {
×
UNCOV
85
      $0.headers = HTTPFields(headers)
×
UNCOV
86
      if $0.headers[.xClientInfo] == nil {
×
UNCOV
87
        $0.headers[.xClientInfo] = "functions-swift/\(version)"
×
UNCOV
88
      }
×
UNCOV
89
    }
×
UNCOV
90
  }
×
91

92
  /// Initializes a new instance of `FunctionsClient`.
93
  ///
94
  /// - Parameters:
95
  ///   - url: The base URL for the functions.
96
  ///   - headers: Headers to be included in the requests. (Default: empty dictionary)
97
  ///   - region: The Region to invoke the functions in.
98
  ///   - logger: SupabaseLogger instance to use.
99
  ///   - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
100
  public convenience init(
101
    url: URL,
102
    headers: [String: String] = [:],
103
    region: FunctionRegion? = nil,
104
    logger: (any SupabaseLogger)? = nil,
105
    fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }
×
106
  ) {
7✔
107
    self.init(url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: fetch)
7✔
108
  }
7✔
109

110
  /// Updates the authorization header.
111
  ///
112
  /// - Parameter token: The new JWT token sent in the authorization header.
113
  public func setAuth(token: String?) {
1✔
114
    mutableState.withValue {
1✔
115
      if let token {
1✔
116
        $0.headers[.authorization] = "Bearer \(token)"
1✔
117
      } else {
1✔
118
        $0.headers[.authorization] = nil
×
119
      }
×
120
    }
1✔
121
  }
1✔
122

123
  /// Invokes a function and decodes the response.
124
  ///
125
  /// - Parameters:
126
  ///   - functionName: The name of the function to invoke.
127
  ///   - options: Options for invoking the function. (Default: empty `FunctionInvokeOptions`)
128
  ///   - decode: A closure to decode the response data and HTTPURLResponse into a `Response`
129
  /// object.
130
  /// - Returns: The decoded `Response` object.
131
  public func invoke<Response>(
132
    _ functionName: String,
133
    options: FunctionInvokeOptions = .init(),
134
    decode: (Data, HTTPURLResponse) throws -> Response
135
  ) async throws -> Response {
14✔
136
    let response = try await rawInvoke(
14✔
137
      functionName: functionName, invokeOptions: options
14✔
138
    )
14✔
139
    return try decode(response.data, response.underlyingResponse)
6✔
140
  }
14✔
141

142
  /// Invokes a function and decodes the response as a specific type.
143
  ///
144
  /// - Parameters:
145
  ///   - functionName: The name of the function to invoke.
146
  ///   - options: Options for invoking the function. (Default: empty `FunctionInvokeOptions`)
147
  ///   - decoder: The JSON decoder to use for decoding the response. (Default: `JSONDecoder()`)
148
  /// - Returns: The decoded object of type `T`.
149
  public func invoke<T: Decodable>(
150
    _ functionName: String,
151
    options: FunctionInvokeOptions = .init(),
152
    decoder: JSONDecoder = JSONDecoder()
153
  ) async throws -> T {
×
154
    try await invoke(functionName, options: options) { data, _ in
×
155
      try decoder.decode(T.self, from: data)
×
156
    }
×
157
  }
×
158

159
  /// Invokes a function without expecting a response.
160
  ///
161
  /// - Parameters:
162
  ///   - functionName: The name of the function to invoke.
163
  ///   - options: Options for invoking the function. (Default: empty `FunctionInvokeOptions`)
164
  public func invoke(
165
    _ functionName: String,
166
    options: FunctionInvokeOptions = .init()
167
  ) async throws {
14✔
168
    try await invoke(functionName, options: options) { _, _ in () }
14✔
169
  }
6✔
170

171
  private func rawInvoke(
172
    functionName: String,
173
    invokeOptions: FunctionInvokeOptions
174
  ) async throws -> Helpers.HTTPResponse {
14✔
175
    let request = buildRequest(functionName: functionName, options: invokeOptions)
14✔
176
    let response = try await http.send(request)
14✔
177

8✔
178
    guard 200..<300 ~= response.statusCode else {
8✔
179
      throw FunctionsError.httpError(code: response.statusCode, data: response.data)
1✔
180
    }
7✔
181

7✔
182
    let isRelayError = response.headers[.xRelayError] == "true"
7✔
183
    if isRelayError {
7✔
184
      throw FunctionsError.relayError
1✔
185
    }
6✔
186

6✔
187
    return response
6✔
188
  }
14✔
189

190
  /// Invokes a function with streamed response.
191
  ///
192
  /// Function MUST return a `text/event-stream` content type for this method to work.
193
  ///
194
  /// - Parameters:
195
  ///   - functionName: The name of the function to invoke.
196
  ///   - invokeOptions: Options for invoking the function.
197
  /// - Returns: A stream of Data.
198
  ///
199
  /// - Warning: Experimental method.
200
  /// - Note: This method doesn't use the same underlying `URLSession` as the remaining methods in the library.
201
  public func _invokeWithStreamedResponse(
202
    _ functionName: String,
203
    options invokeOptions: FunctionInvokeOptions = .init()
204
  ) -> AsyncThrowingStream<Data, any Error> {
×
205
    let (stream, continuation) = AsyncThrowingStream<Data, any Error>.makeStream()
×
206
    let delegate = StreamResponseDelegate(continuation: continuation)
×
207

×
NEW
208
    let session = URLSession(
×
NEW
209
      configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
×
210

×
211
    let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest
×
212

×
213
    let task = session.dataTask(with: urlRequest)
×
214
    task.resume()
×
215

×
216
    continuation.onTermination = { _ in
×
217
      task.cancel()
×
218

×
219
      // Hold a strong reference to delegate until continuation terminates.
×
220
      _ = delegate
×
221
    }
×
222

×
223
    return stream
×
224
  }
×
225

226
  private func buildRequest(functionName: String, options: FunctionInvokeOptions)
227
    -> Helpers.HTTPRequest
228
  {
14✔
229
    var request = HTTPRequest(
14✔
230
      url: url.appendingPathComponent(functionName),
14✔
231
      method: FunctionInvokeOptions.httpMethod(options.method) ?? .post,
14✔
232
      query: options.query,
14✔
233
      headers: mutableState.headers.merging(with: options.headers),
14✔
234
      body: options.body
14✔
235
    )
14✔
236

14✔
237
    if let region = options.region ?? region {
14✔
238
      request.headers[.xRegion] = region
3✔
239
    }
3✔
240

14✔
241
    return request
14✔
242
  }
14✔
243
}
244

245
final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
246
  let continuation: AsyncThrowingStream<Data, any Error>.Continuation
NEW
247
  let lastReceivedData = LockIsolated<Data?>(nil)
×
248

249
  init(continuation: AsyncThrowingStream<Data, any Error>.Continuation) {
×
250
    self.continuation = continuation
×
251
  }
×
252

253
  func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
×
NEW
254
    lastReceivedData.setValue(data)
×
255
    continuation.yield(data)
×
256
  }
×
257

258
  func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?) {
×
259
    continuation.finish(throwing: error)
×
260
  }
×
261

262
  func urlSession(
263
    _: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse,
264
    completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
NEW
265
  ) {
×
NEW
266
    defer {
×
NEW
267
      completionHandler(.allow)
×
NEW
268
    }
×
NEW
269

×
270
    guard let httpResponse = response as? HTTPURLResponse else {
×
271
      continuation.finish(throwing: URLError(.badServerResponse))
×
272
      return
×
273
    }
×
274

×
NEW
275
    guard 200..<300 ~= httpResponse.statusCode else {
×
NEW
276
      let error = FunctionsError.httpError(
×
NEW
277
        code: httpResponse.statusCode,
×
NEW
278
        data: lastReceivedData.value ?? Data()
×
NEW
279
      )
×
280
      continuation.finish(throwing: error)
×
281
      return
×
282
    }
×
283

×
284
    let isRelayError = httpResponse.value(forHTTPHeaderField: "x-relay-error") == "true"
×
285
    if isRelayError {
×
286
      continuation.finish(throwing: FunctionsError.relayError)
×
287
    }
×
288
  }
×
289
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc