2022-01-31 09:10:27 -08:00
|
|
|
// Copyright 2022 Google LLC
|
2022-01-26 15:24:08 -05:00
|
|
|
//
|
2022-01-31 09:10:27 -08:00
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
2022-01-26 15:24:08 -05:00
|
|
|
//
|
2022-01-31 09:10:27 -08:00
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
2022-01-26 15:24:08 -05:00
|
|
|
//
|
2022-01-31 09:10:27 -08:00
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
2022-01-26 15:24:08 -05:00
|
|
|
|
2022-02-07 08:46:44 -09:00
|
|
|
import FirebaseAppCheckInterop
|
2022-02-09 13:11:56 -09:00
|
|
|
import FirebaseAuthInterop
|
2022-01-26 15:24:08 -05:00
|
|
|
import FirebaseCore
|
2022-02-09 13:11:56 -09:00
|
|
|
import FirebaseMessagingInterop
|
2022-01-26 15:24:08 -05:00
|
|
|
import FirebaseSharedSwift
|
2023-11-21 12:12:05 -08:00
|
|
|
import Foundation
|
2022-01-31 09:10:27 -08:00
|
|
|
#if COCOAPODS
|
|
|
|
|
import GTMSessionFetcher
|
|
|
|
|
#else
|
|
|
|
|
import GTMSessionFetcherCore
|
|
|
|
|
#endif
|
2022-01-26 15:24:08 -05:00
|
|
|
|
2025-04-09 16:53:48 -04:00
|
|
|
internal import FirebaseCoreExtension
|
2022-06-08 10:09:55 -04:00
|
|
|
|
2025-03-19 10:26:53 -04:00
|
|
|
final class AtomicBox<T> {
|
|
|
|
|
private var _value: T
|
|
|
|
|
private let lock = NSLock()
|
|
|
|
|
|
|
|
|
|
public init(_ value: T) {
|
|
|
|
|
_value = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func value() -> T {
|
|
|
|
|
lock.withLock {
|
|
|
|
|
_value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
|
public func withLock(_ mutatingBody: (_ value: inout T) -> Void) -> T {
|
|
|
|
|
lock.withLock {
|
|
|
|
|
mutatingBody(&_value)
|
|
|
|
|
return _value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@discardableResult
|
|
|
|
|
public func withLock<R>(_ mutatingBody: (_ value: inout T) throws -> R) rethrows -> R {
|
|
|
|
|
try lock.withLock {
|
|
|
|
|
try mutatingBody(&_value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-07 08:42:49 -09:00
|
|
|
/// File specific constants.
|
2022-01-26 15:24:08 -05:00
|
|
|
private enum Constants {
|
|
|
|
|
static let appCheckTokenHeader = "X-Firebase-AppCheck"
|
|
|
|
|
static let fcmTokenHeader = "Firebase-Instance-ID-Token"
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-07 08:42:49 -09:00
|
|
|
/// Cross SDK constants.
|
2023-11-21 12:12:05 -08:00
|
|
|
enum FunctionsConstants {
|
2022-02-07 08:42:49 -09:00
|
|
|
static let defaultRegion = "us-central1"
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// `Functions` is the client for Cloud Functions for a Firebase project.
|
2022-02-11 15:51:30 -09:00
|
|
|
@objc(FIRFunctions) open class Functions: NSObject {
|
2022-01-26 15:24:08 -05:00
|
|
|
// MARK: - Private Variables
|
|
|
|
|
|
|
|
|
|
/// The network client to use for http requests.
|
|
|
|
|
private let fetcherService: GTMSessionFetcherService
|
2022-04-26 11:04:38 -07:00
|
|
|
/// The projectID to use for all function references.
|
2022-01-26 15:24:08 -05:00
|
|
|
private let projectID: String
|
2022-04-26 11:04:38 -07:00
|
|
|
/// A serializer to encode/decode data and return values.
|
2024-07-27 20:40:16 +02:00
|
|
|
private let serializer = FunctionsSerializer()
|
2022-04-26 11:04:38 -07:00
|
|
|
/// A factory for getting the metadata to include with function calls.
|
2022-01-26 15:24:08 -05:00
|
|
|
private let contextProvider: FunctionsContextProvider
|
|
|
|
|
|
2024-06-03 15:45:09 -07:00
|
|
|
/// A map of active instances, grouped by app. Keys are FirebaseApp names and values are arrays
|
|
|
|
|
/// containing all instances of Functions associated with the given app.
|
2025-04-24 13:59:27 -04:00
|
|
|
private nonisolated(unsafe) static var instances: AtomicBox<[String: [Functions]]> =
|
|
|
|
|
AtomicBox([:])
|
2024-06-03 15:45:09 -07:00
|
|
|
|
2022-04-26 11:04:38 -07:00
|
|
|
/// The custom domain to use for all functions references (optional).
|
2023-11-21 12:12:05 -08:00
|
|
|
let customDomain: String?
|
2022-02-07 08:42:49 -09:00
|
|
|
|
2022-04-26 11:04:38 -07:00
|
|
|
/// The region to use for all function references.
|
2023-11-21 12:12:05 -08:00
|
|
|
let region: String
|
2022-02-07 08:42:49 -09:00
|
|
|
|
2022-03-03 09:10:32 -08:00
|
|
|
// MARK: - Public APIs
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// The current emulator origin, or `nil` if it is not set.
|
2022-03-03 09:10:32 -08:00
|
|
|
open private(set) var emulatorOrigin: String?
|
2022-01-26 15:24:08 -05:00
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client using the default or returns a pre-existing instance if it
|
|
|
|
|
/// already exists.
|
|
|
|
|
/// - Returns: A shared Functions instance initialized with the default `FirebaseApp`.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functions) open class func functions() -> Functions {
|
|
|
|
|
return functions(
|
|
|
|
|
app: FirebaseApp.app(),
|
|
|
|
|
region: FunctionsConstants.defaultRegion,
|
|
|
|
|
customDomain: nil
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client with the given app, or returns a pre-existing
|
|
|
|
|
/// instance if one already exists.
|
|
|
|
|
/// - Parameter app: The app for the Firebase project.
|
|
|
|
|
/// - Returns: A shared Functions instance initialized with the specified `FirebaseApp`.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functionsForApp:) open class func functions(app: FirebaseApp) -> Functions {
|
|
|
|
|
return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: nil)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client with the default app and given region.
|
|
|
|
|
/// - Parameter region: The region for the HTTP trigger, such as `us-central1`.
|
|
|
|
|
/// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a
|
|
|
|
|
/// custom region.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functionsForRegion:) open class func functions(region: String) -> Functions {
|
|
|
|
|
return functions(app: FirebaseApp.app(), region: region, customDomain: nil)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client with the given custom domain or returns a pre-existing
|
|
|
|
|
/// instance if one already exists.
|
|
|
|
|
/// - Parameter customDomain: A custom domain for the HTTP trigger, such as
|
|
|
|
|
/// "https://mydomain.com".
|
|
|
|
|
/// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a
|
|
|
|
|
/// custom HTTP trigger domain.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functionsForCustomDomain:) open class func functions(customDomain: String) -> Functions {
|
|
|
|
|
return functions(app: FirebaseApp.app(),
|
|
|
|
|
region: FunctionsConstants.defaultRegion, customDomain: customDomain)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client with the given app and region, or returns a pre-existing
|
|
|
|
|
/// instance if one already exists.
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - app: The app for the Firebase project.
|
|
|
|
|
/// - region: The region for the HTTP trigger, such as `us-central1`.
|
|
|
|
|
/// - Returns: An instance of `Functions` with a custom app and region.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functionsForApp:region:) open class func functions(app: FirebaseApp,
|
2022-03-03 09:10:32 -08:00
|
|
|
region: String) -> Functions {
|
2022-04-20 16:30:06 -04:00
|
|
|
return functions(app: app, region: region, customDomain: nil)
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
2024-01-29 09:43:20 -08:00
|
|
|
/// Creates a Cloud Functions client with the given app and custom domain, or returns a
|
|
|
|
|
/// pre-existing
|
|
|
|
|
/// instance if one already exists.
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - app: The app for the Firebase project.
|
|
|
|
|
/// - customDomain: A custom domain for the HTTP trigger, such as `https://mydomain.com`.
|
|
|
|
|
/// - Returns: An instance of `Functions` with a custom app and HTTP trigger domain.
|
2022-04-20 16:30:06 -04:00
|
|
|
@objc(functionsForApp:customDomain:) open class func functions(app: FirebaseApp,
|
|
|
|
|
customDomain: String)
|
|
|
|
|
-> Functions {
|
|
|
|
|
return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: customDomain)
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
2023-05-16 15:33:15 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name.
|
|
|
|
|
/// - Parameter name: The name of the Callable HTTPS trigger.
|
|
|
|
|
/// - Returns: A reference to a Callable HTTPS trigger.
|
2022-03-03 09:10:32 -08:00
|
|
|
@objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable {
|
2024-10-03 22:07:46 +02:00
|
|
|
HTTPSCallable(functions: self, url: functionURL(for: name)!)
|
2022-03-03 09:10:32 -08:00
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name and configuration
|
|
|
|
|
/// options.
|
2023-05-16 15:33:15 -07:00
|
|
|
/// - Parameters:
|
|
|
|
|
/// - name: The name of the Callable HTTPS trigger.
|
|
|
|
|
/// - options: The options with which to customize the Callable HTTPS trigger.
|
|
|
|
|
/// - Returns: A reference to a Callable HTTPS trigger.
|
|
|
|
|
@objc(HTTPSCallableWithName:options:) public func httpsCallable(_ name: String,
|
|
|
|
|
options: HTTPSCallableOptions)
|
|
|
|
|
-> HTTPSCallable {
|
2024-10-03 22:07:46 +02:00
|
|
|
HTTPSCallable(functions: self, url: functionURL(for: name)!, options: options)
|
2023-05-16 15:33:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name.
|
|
|
|
|
/// - Parameter url: The URL of the Callable HTTPS trigger.
|
|
|
|
|
/// - Returns: A reference to a Callable HTTPS trigger.
|
2022-04-19 17:57:38 -07:00
|
|
|
@objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable {
|
2022-04-19 14:19:55 -07:00
|
|
|
return HTTPSCallable(functions: self, url: url)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name and configuration
|
|
|
|
|
/// options.
|
2023-05-16 15:33:15 -07:00
|
|
|
/// - Parameters:
|
|
|
|
|
/// - url: The URL of the Callable HTTPS trigger.
|
|
|
|
|
/// - options: The options with which to customize the Callable HTTPS trigger.
|
|
|
|
|
/// - Returns: A reference to a Callable HTTPS trigger.
|
|
|
|
|
@objc(HTTPSCallableWithURL:options:) public func httpsCallable(_ url: URL,
|
|
|
|
|
options: HTTPSCallableOptions)
|
|
|
|
|
-> HTTPSCallable {
|
|
|
|
|
return HTTPSCallable(functions: self, url: url, options: options)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an
|
|
|
|
|
/// `Encodable`
|
2022-03-03 09:10:32 -08:00
|
|
|
/// request and the type of a `Decodable` response.
|
2023-05-16 15:33:15 -07:00
|
|
|
/// - Parameters:
|
|
|
|
|
/// - name: The name of the Callable HTTPS trigger
|
|
|
|
|
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
|
|
|
|
|
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
|
|
|
|
|
/// - encoder: The encoder instance to use to perform encoding.
|
|
|
|
|
/// - decoder: The decoder instance to use to perform decoding.
|
2023-07-11 15:54:51 -07:00
|
|
|
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud
|
|
|
|
|
/// Functions invocations.
|
2022-03-03 09:10:32 -08:00
|
|
|
open func httpsCallable<Request: Encodable,
|
|
|
|
|
Response: Decodable>(_ name: String,
|
|
|
|
|
requestAs: Request.Type = Request.self,
|
|
|
|
|
responseAs: Response.Type = Response.self,
|
|
|
|
|
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
|
|
|
|
|
),
|
|
|
|
|
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
|
|
|
|
|
))
|
|
|
|
|
-> Callable<Request, Response> {
|
2023-05-16 15:33:15 -07:00
|
|
|
return Callable(
|
|
|
|
|
callable: httpsCallable(name),
|
|
|
|
|
encoder: encoder,
|
|
|
|
|
decoder: decoder
|
|
|
|
|
)
|
2022-03-03 09:10:32 -08:00
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an
|
|
|
|
|
/// `Encodable`
|
2022-04-19 14:19:55 -07:00
|
|
|
/// request and the type of a `Decodable` response.
|
2023-05-16 15:33:15 -07:00
|
|
|
/// - Parameters:
|
|
|
|
|
/// - name: The name of the Callable HTTPS trigger
|
|
|
|
|
/// - options: The options with which to customize the Callable HTTPS trigger.
|
|
|
|
|
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
|
|
|
|
|
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
|
|
|
|
|
/// - encoder: The encoder instance to use to perform encoding.
|
|
|
|
|
/// - decoder: The decoder instance to use to perform decoding.
|
2023-07-11 15:54:51 -07:00
|
|
|
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud
|
|
|
|
|
/// Functions invocations.
|
2023-05-16 15:33:15 -07:00
|
|
|
open func httpsCallable<Request: Encodable,
|
|
|
|
|
Response: Decodable>(_ name: String,
|
|
|
|
|
options: HTTPSCallableOptions,
|
|
|
|
|
requestAs: Request.Type = Request.self,
|
|
|
|
|
responseAs: Response.Type = Response.self,
|
|
|
|
|
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
|
|
|
|
|
),
|
|
|
|
|
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
|
|
|
|
|
))
|
|
|
|
|
-> Callable<Request, Response> {
|
|
|
|
|
return Callable(
|
|
|
|
|
callable: httpsCallable(name, options: options),
|
|
|
|
|
encoder: encoder,
|
|
|
|
|
decoder: decoder
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an
|
|
|
|
|
/// `Encodable`
|
2023-05-16 15:33:15 -07:00
|
|
|
/// request and the type of a `Decodable` response.
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - url: The url of the Callable HTTPS trigger
|
|
|
|
|
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
|
|
|
|
|
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
|
|
|
|
|
/// - encoder: The encoder instance to use to perform encoding.
|
|
|
|
|
/// - decoder: The decoder instance to use to perform decoding.
|
2023-07-11 15:54:51 -07:00
|
|
|
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud
|
|
|
|
|
/// Functions invocations.
|
2022-04-19 14:19:55 -07:00
|
|
|
open func httpsCallable<Request: Encodable,
|
2022-04-19 17:57:38 -07:00
|
|
|
Response: Decodable>(_ url: URL,
|
2022-04-19 14:19:55 -07:00
|
|
|
requestAs: Request.Type = Request.self,
|
|
|
|
|
responseAs: Response.Type = Response.self,
|
|
|
|
|
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
|
|
|
|
|
),
|
|
|
|
|
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
|
|
|
|
|
))
|
|
|
|
|
-> Callable<Request, Response> {
|
2023-05-16 15:33:15 -07:00
|
|
|
return Callable(
|
|
|
|
|
callable: httpsCallable(url),
|
|
|
|
|
encoder: encoder,
|
|
|
|
|
decoder: decoder
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-11 15:54:51 -07:00
|
|
|
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an
|
|
|
|
|
/// `Encodable`
|
2023-05-16 15:33:15 -07:00
|
|
|
/// request and the type of a `Decodable` response.
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - url: The url of the Callable HTTPS trigger
|
|
|
|
|
/// - options: The options with which to customize the Callable HTTPS trigger.
|
|
|
|
|
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
|
|
|
|
|
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
|
|
|
|
|
/// - encoder: The encoder instance to use to perform encoding.
|
|
|
|
|
/// - decoder: The decoder instance to use to perform decoding.
|
2023-07-11 15:54:51 -07:00
|
|
|
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud
|
|
|
|
|
/// Functions invocations.
|
2023-05-16 15:33:15 -07:00
|
|
|
open func httpsCallable<Request: Encodable,
|
|
|
|
|
Response: Decodable>(_ url: URL,
|
|
|
|
|
options: HTTPSCallableOptions,
|
|
|
|
|
requestAs: Request.Type = Request.self,
|
|
|
|
|
responseAs: Response.Type = Response.self,
|
|
|
|
|
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
|
|
|
|
|
),
|
|
|
|
|
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
|
|
|
|
|
))
|
|
|
|
|
-> Callable<Request, Response> {
|
|
|
|
|
return Callable(
|
|
|
|
|
callable: httpsCallable(url, options: options),
|
|
|
|
|
encoder: encoder,
|
|
|
|
|
decoder: decoder
|
|
|
|
|
)
|
2022-04-19 14:19:55 -07:00
|
|
|
}
|
|
|
|
|
|
2022-03-03 09:10:32 -08:00
|
|
|
/**
|
|
|
|
|
* Changes this instance to point to a Cloud Functions emulator running locally.
|
|
|
|
|
* See https://firebase.google.com/docs/functions/local-emulator
|
2022-04-26 11:04:38 -07:00
|
|
|
* - Parameters:
|
|
|
|
|
* - host: The host of the local emulator, such as "localhost".
|
|
|
|
|
* - port: The port of the local emulator, for example 5005.
|
2022-03-03 09:10:32 -08:00
|
|
|
*/
|
|
|
|
|
@objc open func useEmulator(withHost host: String, port: Int) {
|
|
|
|
|
let prefix = host.hasPrefix("http") ? "" : "http://"
|
|
|
|
|
let origin = String(format: "\(prefix)\(host):%li", port)
|
|
|
|
|
emulatorOrigin = origin
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Private Funcs (or Internal for tests)
|
|
|
|
|
|
2022-04-20 16:30:06 -04:00
|
|
|
/// Solely used to have one precondition and one location where we fetch from the container. This
|
2023-07-11 15:54:51 -07:00
|
|
|
/// previously was avoided due to default arguments but that doesn't work well with Obj-C
|
|
|
|
|
/// compatibility.
|
2022-04-20 16:30:06 -04:00
|
|
|
private class func functions(app: FirebaseApp?, region: String,
|
|
|
|
|
customDomain: String?) -> Functions {
|
2024-06-03 15:45:09 -07:00
|
|
|
guard let app else {
|
|
|
|
|
fatalError("`FirebaseApp.configure()` needs to be called before using Functions.")
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 10:26:53 -04:00
|
|
|
return instances.withLock { instances in
|
|
|
|
|
if let associatedInstances = instances[app.name] {
|
|
|
|
|
for instance in associatedInstances {
|
|
|
|
|
// Domains may be nil, so handle with care.
|
|
|
|
|
var equalDomains = false
|
|
|
|
|
if let instanceCustomDomain = instance.customDomain {
|
|
|
|
|
equalDomains = instanceCustomDomain == customDomain
|
|
|
|
|
} else {
|
|
|
|
|
equalDomains = customDomain == nil
|
|
|
|
|
}
|
|
|
|
|
// Check if it's a match.
|
|
|
|
|
if instance.region == region, equalDomains {
|
|
|
|
|
return instance
|
|
|
|
|
}
|
2024-06-03 15:45:09 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-19 10:26:53 -04:00
|
|
|
let newInstance = Functions(app: app, region: region, customDomain: customDomain)
|
|
|
|
|
let existingInstances = instances[app.name, default: []]
|
|
|
|
|
instances[app.name] = existingInstances + [newInstance]
|
|
|
|
|
return newInstance
|
2024-06-03 15:45:09 -07:00
|
|
|
}
|
2022-04-20 16:30:06 -04:00
|
|
|
}
|
|
|
|
|
|
2023-11-21 12:12:05 -08:00
|
|
|
@objc init(projectID: String,
|
|
|
|
|
region: String,
|
|
|
|
|
customDomain: String?,
|
|
|
|
|
auth: AuthInterop?,
|
|
|
|
|
messaging: MessagingInterop?,
|
|
|
|
|
appCheck: AppCheckInterop?,
|
|
|
|
|
fetcherService: GTMSessionFetcherService = GTMSessionFetcherService()) {
|
2022-03-03 09:10:32 -08:00
|
|
|
self.projectID = projectID
|
|
|
|
|
self.region = region
|
|
|
|
|
self.customDomain = customDomain
|
|
|
|
|
emulatorOrigin = nil
|
|
|
|
|
contextProvider = FunctionsContextProvider(auth: auth,
|
|
|
|
|
messaging: messaging,
|
|
|
|
|
appCheck: appCheck)
|
|
|
|
|
self.fetcherService = fetcherService
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-03 14:48:07 -08:00
|
|
|
/// Using the component system for initialization.
|
2023-11-21 12:12:05 -08:00
|
|
|
convenience init(app: FirebaseApp,
|
|
|
|
|
region: String,
|
|
|
|
|
customDomain: String?) {
|
2022-02-07 08:42:49 -09:00
|
|
|
// TODO: These are not optionals, but they should be.
|
|
|
|
|
let auth = ComponentType<AuthInterop>.instance(for: AuthInterop.self, in: app.container)
|
|
|
|
|
let messaging = ComponentType<MessagingInterop>.instance(for: MessagingInterop.self,
|
|
|
|
|
in: app.container)
|
|
|
|
|
let appCheck = ComponentType<AppCheckInterop>.instance(for: AppCheckInterop.self,
|
|
|
|
|
in: app.container)
|
|
|
|
|
|
2022-02-07 08:37:53 -09:00
|
|
|
guard let projectID = app.options.projectID else {
|
|
|
|
|
fatalError("Firebase Functions requires the projectID to be set in the App's Options.")
|
|
|
|
|
}
|
|
|
|
|
self.init(projectID: projectID,
|
2022-01-26 15:24:08 -05:00
|
|
|
region: region,
|
|
|
|
|
customDomain: customDomain,
|
2022-02-07 08:42:49 -09:00
|
|
|
auth: auth,
|
|
|
|
|
messaging: messaging,
|
|
|
|
|
appCheck: appCheck)
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
2024-10-03 22:07:46 +02:00
|
|
|
func functionURL(for name: String) -> URL? {
|
2022-01-26 15:24:08 -05:00
|
|
|
assert(!name.isEmpty, "Name cannot be empty")
|
|
|
|
|
|
|
|
|
|
// Check if we're using the emulator
|
2024-05-03 17:50:20 +02:00
|
|
|
if let emulatorOrigin {
|
2024-10-03 22:07:46 +02:00
|
|
|
return URL(string: "\(emulatorOrigin)/\(projectID)/\(region)/\(name)")
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check the custom domain.
|
2024-05-03 17:50:20 +02:00
|
|
|
if let customDomain {
|
2024-10-03 22:07:46 +02:00
|
|
|
return URL(string: "\(customDomain)/\(name)")
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
2024-10-03 22:07:46 +02:00
|
|
|
return URL(string: "https://\(region)-\(projectID).cloudfunctions.net/\(name)")
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
2024-10-29 23:44:26 +01:00
|
|
|
@available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *)
|
|
|
|
|
func callFunction(at url: URL,
|
|
|
|
|
withObject data: Any?,
|
|
|
|
|
options: HTTPSCallableOptions?,
|
|
|
|
|
timeout: TimeInterval) async throws -> HTTPSCallableResult {
|
|
|
|
|
let context = try await contextProvider.context(options: options)
|
|
|
|
|
let fetcher = try makeFetcher(
|
|
|
|
|
url: url,
|
|
|
|
|
data: data,
|
|
|
|
|
options: options,
|
|
|
|
|
timeout: timeout,
|
|
|
|
|
context: context
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
let rawData = try await fetcher.beginFetch()
|
2025-03-03 20:53:39 +04:00
|
|
|
return try callableResult(fromResponseData: rawData, endpointURL: url)
|
2024-10-29 23:44:26 +01:00
|
|
|
} catch {
|
2025-03-03 20:53:39 +04:00
|
|
|
throw processedError(fromResponseError: error, endpointURL: url)
|
2024-10-29 23:44:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-03 22:07:46 +02:00
|
|
|
func callFunction(at url: URL,
|
2023-11-21 12:12:05 -08:00
|
|
|
withObject data: Any?,
|
|
|
|
|
options: HTTPSCallableOptions?,
|
|
|
|
|
timeout: TimeInterval,
|
2025-04-25 20:19:31 -04:00
|
|
|
completion: @escaping @MainActor (Result<HTTPSCallableResult, Error>) -> Void) {
|
2022-04-19 14:19:55 -07:00
|
|
|
// Get context first.
|
2023-05-16 15:33:15 -07:00
|
|
|
contextProvider.getContext(options: options) { context, error in
|
2022-04-19 14:19:55 -07:00
|
|
|
// Note: context is always non-nil since some checks could succeed, we're only failing if
|
|
|
|
|
// there's an error.
|
2024-05-03 17:50:20 +02:00
|
|
|
if let error {
|
2025-04-25 20:19:31 -04:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
completion(.failure(error))
|
|
|
|
|
}
|
2022-04-19 14:19:55 -07:00
|
|
|
} else {
|
|
|
|
|
self.callFunction(url: url,
|
|
|
|
|
withObject: data,
|
2023-05-16 15:33:15 -07:00
|
|
|
options: options,
|
2022-04-19 14:19:55 -07:00
|
|
|
timeout: timeout,
|
|
|
|
|
context: context,
|
|
|
|
|
completion: completion)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func callFunction(url: URL,
|
2022-01-31 09:10:27 -08:00
|
|
|
withObject data: Any?,
|
2023-05-16 15:33:15 -07:00
|
|
|
options: HTTPSCallableOptions?,
|
2022-01-31 09:10:27 -08:00
|
|
|
timeout: TimeInterval,
|
|
|
|
|
context: FunctionsContext,
|
2025-04-25 20:19:31 -04:00
|
|
|
completion: @escaping @MainActor (Result<HTTPSCallableResult, Error>) -> Void) {
|
2024-10-29 23:44:26 +01:00
|
|
|
let fetcher: GTMSessionFetcher
|
2022-01-26 15:24:08 -05:00
|
|
|
do {
|
2024-10-29 23:44:26 +01:00
|
|
|
fetcher = try makeFetcher(
|
|
|
|
|
url: url,
|
|
|
|
|
data: data,
|
|
|
|
|
options: options,
|
|
|
|
|
timeout: timeout,
|
|
|
|
|
context: context
|
|
|
|
|
)
|
2022-01-26 15:24:08 -05:00
|
|
|
} catch {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
completion(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 23:44:26 +01:00
|
|
|
fetcher.beginFetch { [self] data, error in
|
|
|
|
|
let result: Result<HTTPSCallableResult, any Error>
|
2024-11-18 16:20:38 +01:00
|
|
|
if let error {
|
2025-03-03 20:53:39 +04:00
|
|
|
result = .failure(processedError(fromResponseError: error, endpointURL: url))
|
2024-11-18 16:20:38 +01:00
|
|
|
} else if let data {
|
|
|
|
|
do {
|
2025-03-03 20:53:39 +04:00
|
|
|
result = try .success(callableResult(fromResponseData: data, endpointURL: url))
|
2024-11-18 16:20:38 +01:00
|
|
|
} catch {
|
|
|
|
|
result = .failure(error)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result = .failure(FunctionsError(.internal))
|
2024-10-29 23:44:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
completion(result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 14:37:52 -04:00
|
|
|
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
|
|
|
|
|
func stream(at url: URL,
|
|
|
|
|
data: Any?,
|
|
|
|
|
options: HTTPSCallableOptions?,
|
|
|
|
|
timeout: TimeInterval)
|
|
|
|
|
-> AsyncThrowingStream<JSONStreamResponse, Error> {
|
|
|
|
|
AsyncThrowingStream { continuation in
|
|
|
|
|
Task {
|
|
|
|
|
let urlRequest: URLRequest
|
|
|
|
|
do {
|
|
|
|
|
let context = try await contextProvider.context(options: options)
|
|
|
|
|
urlRequest = try makeRequestForStreamableContent(
|
|
|
|
|
url: url,
|
|
|
|
|
data: data,
|
|
|
|
|
options: options,
|
|
|
|
|
timeout: timeout,
|
|
|
|
|
context: context
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
continuation.finish(throwing: FunctionsError(
|
|
|
|
|
.invalidArgument,
|
|
|
|
|
userInfo: [NSUnderlyingErrorKey: error]
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stream: URLSession.AsyncBytes
|
|
|
|
|
let rawResponse: URLResponse
|
|
|
|
|
do {
|
|
|
|
|
(stream, rawResponse) = try await URLSession.shared.bytes(for: urlRequest)
|
|
|
|
|
} catch {
|
|
|
|
|
continuation.finish(throwing: FunctionsError(
|
|
|
|
|
.unavailable,
|
|
|
|
|
userInfo: [NSUnderlyingErrorKey: error]
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the status code is an HTTP response.
|
|
|
|
|
guard let response = rawResponse as? HTTPURLResponse else {
|
|
|
|
|
continuation.finish(
|
|
|
|
|
throwing: FunctionsError(
|
|
|
|
|
.unavailable,
|
|
|
|
|
userInfo: [NSLocalizedDescriptionKey: "Response was not an HTTP response."]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the status code is a 200.
|
|
|
|
|
guard response.statusCode == 200 else {
|
|
|
|
|
continuation.finish(
|
|
|
|
|
throwing: FunctionsError(
|
|
|
|
|
httpStatusCode: response.statusCode,
|
|
|
|
|
region: region,
|
|
|
|
|
url: url,
|
|
|
|
|
body: nil,
|
|
|
|
|
serializer: serializer
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
for try await line in stream.lines {
|
|
|
|
|
guard line.hasPrefix("data:") else {
|
|
|
|
|
continuation.finish(
|
|
|
|
|
throwing: FunctionsError(
|
|
|
|
|
.dataLoss,
|
|
|
|
|
userInfo: [NSLocalizedDescriptionKey: "Unexpected format for streamed response."]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
// We can assume 5 characters since it's utf-8 encoded, removing `data:`.
|
|
|
|
|
let jsonText = String(line.dropFirst(5))
|
|
|
|
|
let data = try jsonData(jsonText: jsonText)
|
|
|
|
|
// Handle the content and parse it.
|
|
|
|
|
let content = try callableStreamResult(fromResponseData: data, endpointURL: url)
|
|
|
|
|
continuation.yield(content)
|
|
|
|
|
} catch {
|
|
|
|
|
continuation.finish(throwing: error)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
continuation.finish(
|
|
|
|
|
throwing: FunctionsError(
|
|
|
|
|
.dataLoss,
|
|
|
|
|
userInfo: [
|
|
|
|
|
NSLocalizedDescriptionKey: "Unexpected format for streamed response.",
|
|
|
|
|
NSUnderlyingErrorKey: error,
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continuation.finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-24 13:59:27 -04:00
|
|
|
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
|
|
|
|
|
private func callableStreamResult(fromResponseData data: Data,
|
|
|
|
|
endpointURL url: URL) throws -> sending JSONStreamResponse {
|
|
|
|
|
let data = try processedData(fromResponseData: data, endpointURL: url)
|
2025-03-10 14:37:52 -04:00
|
|
|
|
2025-04-24 13:59:27 -04:00
|
|
|
let responseJSONObject: Any
|
|
|
|
|
do {
|
|
|
|
|
responseJSONObject = try JSONSerialization.jsonObject(with: data)
|
|
|
|
|
} catch {
|
|
|
|
|
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
|
2025-03-10 14:37:52 -04:00
|
|
|
}
|
2025-03-19 10:26:53 -04:00
|
|
|
|
2025-04-24 13:59:27 -04:00
|
|
|
guard let responseJSON = responseJSONObject as? [String: Any] else {
|
|
|
|
|
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
|
|
|
|
|
throw FunctionsError(.dataLoss, userInfo: userInfo)
|
2025-03-19 10:26:53 -04:00
|
|
|
}
|
2025-04-24 13:59:27 -04:00
|
|
|
|
|
|
|
|
if let _ = responseJSON["result"] {
|
|
|
|
|
return .result(responseJSON)
|
|
|
|
|
} else if let _ = responseJSON["message"] {
|
|
|
|
|
return .message(responseJSON)
|
|
|
|
|
} else {
|
|
|
|
|
throw FunctionsError(
|
|
|
|
|
.dataLoss,
|
|
|
|
|
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-10 14:37:52 -04:00
|
|
|
|
|
|
|
|
private func jsonData(jsonText: String) throws -> Data {
|
|
|
|
|
guard let data = jsonText.data(using: .utf8) else {
|
|
|
|
|
throw FunctionsError(.dataLoss, userInfo: [
|
|
|
|
|
NSUnderlyingErrorKey: DecodingError.dataCorrupted(DecodingError.Context(
|
|
|
|
|
codingPath: [],
|
|
|
|
|
debugDescription: "Could not parse response as UTF8."
|
|
|
|
|
)),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func makeRequestForStreamableContent(url: URL,
|
|
|
|
|
data: Any?,
|
|
|
|
|
options: HTTPSCallableOptions?,
|
|
|
|
|
timeout: TimeInterval,
|
|
|
|
|
context: FunctionsContext) throws
|
|
|
|
|
-> URLRequest {
|
|
|
|
|
var urlRequest = URLRequest(
|
|
|
|
|
url: url,
|
|
|
|
|
cachePolicy: .useProtocolCachePolicy,
|
|
|
|
|
timeoutInterval: timeout
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let data = data ?? NSNull()
|
|
|
|
|
let encoded = try serializer.encode(data)
|
|
|
|
|
let body = ["data": encoded]
|
|
|
|
|
let payload = try JSONSerialization.data(withJSONObject: body, options: [.fragmentsAllowed])
|
|
|
|
|
urlRequest.httpBody = payload
|
|
|
|
|
|
|
|
|
|
// Set the headers for starting a streaming session.
|
|
|
|
|
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
|
urlRequest.setValue("text/event-stream", forHTTPHeaderField: "Accept")
|
|
|
|
|
urlRequest.httpMethod = "POST"
|
|
|
|
|
|
|
|
|
|
if let authToken = context.authToken {
|
|
|
|
|
let value = "Bearer \(authToken)"
|
|
|
|
|
urlRequest.setValue(value, forHTTPHeaderField: "Authorization")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let fcmToken = context.fcmToken {
|
|
|
|
|
urlRequest.setValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if options?.requireLimitedUseAppCheckTokens == true {
|
|
|
|
|
if let appCheckToken = context.limitedUseAppCheckToken {
|
|
|
|
|
urlRequest.setValue(
|
|
|
|
|
appCheckToken,
|
|
|
|
|
forHTTPHeaderField: Constants.appCheckTokenHeader
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else if let appCheckToken = context.appCheckToken {
|
|
|
|
|
urlRequest.setValue(
|
|
|
|
|
appCheckToken,
|
|
|
|
|
forHTTPHeaderField: Constants.appCheckTokenHeader
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return urlRequest
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 23:44:26 +01:00
|
|
|
private func makeFetcher(url: URL,
|
|
|
|
|
data: Any?,
|
|
|
|
|
options: HTTPSCallableOptions?,
|
|
|
|
|
timeout: TimeInterval,
|
|
|
|
|
context: FunctionsContext) throws -> GTMSessionFetcher {
|
|
|
|
|
let request = URLRequest(
|
|
|
|
|
url: url,
|
|
|
|
|
cachePolicy: .useProtocolCachePolicy,
|
|
|
|
|
timeoutInterval: timeout
|
|
|
|
|
)
|
|
|
|
|
let fetcher = fetcherService.fetcher(with: request)
|
|
|
|
|
|
|
|
|
|
let data = data ?? NSNull()
|
|
|
|
|
let encoded = try serializer.encode(data)
|
|
|
|
|
let body = ["data": encoded]
|
|
|
|
|
let payload = try JSONSerialization.data(withJSONObject: body)
|
|
|
|
|
fetcher.bodyData = payload
|
|
|
|
|
|
2022-01-26 15:24:08 -05:00
|
|
|
// Set the headers.
|
|
|
|
|
fetcher.setRequestValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
|
if let authToken = context.authToken {
|
|
|
|
|
let value = "Bearer \(authToken)"
|
|
|
|
|
fetcher.setRequestValue(value, forHTTPHeaderField: "Authorization")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let fcmToken = context.fcmToken {
|
|
|
|
|
fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-16 15:33:15 -07:00
|
|
|
if options?.requireLimitedUseAppCheckTokens == true {
|
|
|
|
|
if let appCheckToken = context.limitedUseAppCheckToken {
|
|
|
|
|
fetcher.setRequestValue(
|
|
|
|
|
appCheckToken,
|
|
|
|
|
forHTTPHeaderField: Constants.appCheckTokenHeader
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else if let appCheckToken = context.appCheckToken {
|
|
|
|
|
fetcher.setRequestValue(
|
|
|
|
|
appCheckToken,
|
|
|
|
|
forHTTPHeaderField: Constants.appCheckTokenHeader
|
|
|
|
|
)
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Override normal security rules if this is a local test.
|
|
|
|
|
if emulatorOrigin != nil {
|
|
|
|
|
fetcher.allowLocalhostRequest = true
|
|
|
|
|
fetcher.allowedInsecureSchemes = ["http"]
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 23:44:26 +01:00
|
|
|
return fetcher
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|
2024-08-09 17:18:28 +02:00
|
|
|
|
2025-03-03 20:53:39 +04:00
|
|
|
private func processedError(fromResponseError error: any Error,
|
|
|
|
|
endpointURL url: URL) -> any Error {
|
2024-11-18 16:20:38 +01:00
|
|
|
let error = error as NSError
|
|
|
|
|
let localError: (any Error)? = if error.domain == kGTMSessionFetcherStatusDomain {
|
|
|
|
|
FunctionsError(
|
|
|
|
|
httpStatusCode: error.code,
|
2025-03-03 20:53:39 +04:00
|
|
|
region: region,
|
|
|
|
|
url: url,
|
2024-11-18 16:20:38 +01:00
|
|
|
body: error.userInfo["data"] as? Data,
|
|
|
|
|
serializer: serializer
|
|
|
|
|
)
|
|
|
|
|
} else if error.domain == NSURLErrorDomain, error.code == NSURLErrorTimedOut {
|
|
|
|
|
FunctionsError(.deadlineExceeded)
|
|
|
|
|
} else { nil }
|
|
|
|
|
|
|
|
|
|
return localError ?? error
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-03 20:53:39 +04:00
|
|
|
private func callableResult(fromResponseData data: Data,
|
2025-04-25 20:19:31 -04:00
|
|
|
endpointURL url: URL) throws -> sending HTTPSCallableResult {
|
2025-03-03 20:53:39 +04:00
|
|
|
let processedData = try processedData(fromResponseData: data, endpointURL: url)
|
2024-10-29 23:44:26 +01:00
|
|
|
let json = try responseDataJSON(from: processedData)
|
|
|
|
|
let payload = try serializer.decode(json)
|
2025-02-28 23:53:36 +01:00
|
|
|
return HTTPSCallableResult(data: payload)
|
2024-10-29 23:44:26 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-03 20:53:39 +04:00
|
|
|
private func processedData(fromResponseData data: Data, endpointURL url: URL) throws -> Data {
|
2024-11-18 16:20:38 +01:00
|
|
|
// `data` might specify a custom error. If so, throw the error.
|
2025-03-03 20:53:39 +04:00
|
|
|
if let bodyError = FunctionsError(
|
|
|
|
|
httpStatusCode: 200,
|
|
|
|
|
region: region,
|
|
|
|
|
url: url,
|
|
|
|
|
body: data,
|
|
|
|
|
serializer: serializer
|
|
|
|
|
) {
|
2024-08-09 17:18:28 +02:00
|
|
|
throw bodyError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-25 20:19:31 -04:00
|
|
|
private func responseDataJSON(from data: Data) throws -> sending Any {
|
2024-08-09 17:18:28 +02:00
|
|
|
let responseJSONObject = try JSONSerialization.jsonObject(with: data)
|
|
|
|
|
|
|
|
|
|
guard let responseJSON = responseJSONObject as? NSDictionary else {
|
|
|
|
|
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
|
2024-09-10 20:27:09 +02:00
|
|
|
throw FunctionsError(.internal, userInfo: userInfo)
|
2024-08-09 17:18:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// `result` is checked for backwards compatibility:
|
|
|
|
|
guard let dataJSON = responseJSON["data"] ?? responseJSON["result"] else {
|
|
|
|
|
let userInfo = [NSLocalizedDescriptionKey: "Response is missing data field."]
|
2024-09-10 20:27:09 +02:00
|
|
|
throw FunctionsError(.internal, userInfo: userInfo)
|
2024-08-09 17:18:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataJSON
|
|
|
|
|
}
|
2022-01-26 15:24:08 -05:00
|
|
|
}
|