Callable+Codable.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. @preconcurrency import FirebaseSharedSwift
  15. import Foundation
  16. /// A `Callable` is a reference to a particular Callable HTTPS trigger in Cloud Functions.
  17. ///
  18. /// - Note: If the Callable HTTPS trigger accepts no parameters, ``Never`` can be used for
  19. /// iOS 17.0+. Otherwise, a simple encodable placeholder type (e.g.,
  20. /// `struct EmptyRequest: Encodable {}`) can be used.
  21. public struct Callable<Request: Encodable, Response: Decodable>: Sendable {
  22. /// The timeout to use when calling the function. Defaults to 70 seconds.
  23. public var timeoutInterval: TimeInterval {
  24. get {
  25. callable.timeoutInterval
  26. }
  27. set {
  28. callable.timeoutInterval = newValue
  29. }
  30. }
  31. enum CallableError: Error {
  32. case internalError
  33. }
  34. private let callable: HTTPSCallable
  35. private let encoder: FirebaseDataEncoder
  36. private let decoder: FirebaseDataDecoder
  37. init(callable: HTTPSCallable, encoder: FirebaseDataEncoder, decoder: FirebaseDataDecoder) {
  38. self.callable = callable
  39. self.encoder = encoder
  40. self.decoder = decoder
  41. }
  42. /// Executes this Callable HTTPS trigger asynchronously.
  43. ///
  44. /// The data passed into the trigger must be of the generic `Request` type:
  45. ///
  46. /// The request to the Cloud Functions backend made by this method automatically includes a
  47. /// FCM token to identify the app instance. If a user is logged in with Firebase
  48. /// Auth, an auth ID token for the user is also automatically included.
  49. ///
  50. /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
  51. /// information
  52. /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
  53. /// resumes with a new FCM Token the next time you call this method.
  54. ///
  55. /// - Parameter data: Parameters to pass to the trigger.
  56. /// - Parameter completion: The block to call when the HTTPS request has completed.
  57. public func call(_ data: Request,
  58. completion: @escaping @MainActor (Result<Response, Error>)
  59. -> Void) {
  60. do {
  61. let encoded = try encoder.encode(data)
  62. callable.call(encoded) { result, error in
  63. do {
  64. if let result {
  65. let decoded = try decoder.decode(Response.self, from: result.data)
  66. completion(.success(decoded))
  67. } else if let error {
  68. completion(.failure(error))
  69. } else {
  70. completion(.failure(CallableError.internalError))
  71. }
  72. } catch {
  73. completion(.failure(error))
  74. }
  75. }
  76. } catch {
  77. DispatchQueue.main.async {
  78. completion(.failure(error))
  79. }
  80. }
  81. }
  82. /// Creates a directly callable function.
  83. ///
  84. /// This allows users to call a HTTPS Callable Function like a normal Swift function:
  85. /// ```swift
  86. /// let greeter = functions.httpsCallable("greeter",
  87. /// requestType: GreetingRequest.self,
  88. /// responseType: GreetingResponse.self)
  89. /// greeter(data) { result in
  90. /// print(result.greeting)
  91. /// }
  92. /// ```
  93. /// You can also call a HTTPS Callable function using the following syntax:
  94. /// ```swift
  95. /// let greeter: Callable<GreetingRequest, GreetingResponse> =
  96. /// functions.httpsCallable("greeter")
  97. /// greeter(data) { result in
  98. /// print(result.greeting)
  99. /// }
  100. /// ```
  101. /// - Parameters:
  102. /// - data: Parameters to pass to the trigger.
  103. /// - completion: The block to call when the HTTPS request has completed.
  104. public func callAsFunction(_ data: Request,
  105. completion: @escaping @MainActor (Result<Response, Error>)
  106. -> Void) {
  107. call(data, completion: completion)
  108. }
  109. /// Executes this Callable HTTPS trigger asynchronously.
  110. ///
  111. /// The data passed into the trigger must be of the generic `Request` type:
  112. ///
  113. /// The request to the Cloud Functions backend made by this method automatically includes a
  114. /// FCM token to identify the app instance. If a user is logged in with Firebase
  115. /// Auth, an auth ID token for the user is also automatically included.
  116. ///
  117. /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
  118. /// information
  119. /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
  120. /// resumes with a new FCM Token the next time you call this method.
  121. ///
  122. /// - Parameter data: The `Request` representing the data to pass to the trigger.
  123. ///
  124. /// - Throws: An error if any value throws an error during encoding or decoding.
  125. /// - Throws: An error if the callable fails to complete
  126. ///
  127. /// - Returns: The decoded `Response` value
  128. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  129. public func call(_ data: Request) async throws -> Response {
  130. let encoded = try encoder.encode(data)
  131. let result = try await callable.call(encoded)
  132. return try decoder.decode(Response.self, from: result.data)
  133. }
  134. /// Creates a directly callable function.
  135. ///
  136. /// This allows users to call a HTTPS Callable Function like a normal Swift function:
  137. /// ```swift
  138. /// let greeter = functions.httpsCallable("greeter",
  139. /// requestType: GreetingRequest.self,
  140. /// responseType: GreetingResponse.self)
  141. /// let result = try await greeter(data)
  142. /// print(result.greeting)
  143. /// ```
  144. /// You can also call a HTTPS Callable function using the following syntax:
  145. /// ```swift
  146. /// let greeter: Callable<GreetingRequest, GreetingResponse> =
  147. /// functions.httpsCallable("greeter")
  148. /// let result = try await greeter(data)
  149. /// print(result.greeting)
  150. /// ```
  151. /// - Parameters:
  152. /// - data: Parameters to pass to the trigger.
  153. /// - Returns: The decoded `Response` value
  154. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  155. public func callAsFunction(_ data: Request) async throws -> Response {
  156. return try await call(data)
  157. }
  158. }
  159. /// Used to determine when a `StreamResponse<_, _>` is being decoded.
  160. private protocol StreamResponseProtocol {}
  161. /// A convenience type used to receive both the streaming callable function's yielded messages and
  162. /// its return value.
  163. ///
  164. /// This can be used as the generic `Response` parameter to ``Callable`` to receive both the
  165. /// yielded messages and final return value of the streaming callable function.
  166. @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
  167. public enum StreamResponse<Message: Decodable & Sendable, Result: Decodable & Sendable>: Decodable,
  168. Sendable,
  169. StreamResponseProtocol {
  170. /// The message yielded by the callable function.
  171. case message(Message)
  172. /// The final result returned by the callable function.
  173. case result(Result)
  174. private enum CodingKeys: String, CodingKey {
  175. case message
  176. case result
  177. }
  178. public init(from decoder: any Decoder) throws {
  179. do {
  180. let container = try decoder
  181. .container(keyedBy: Self<Message, Result>.CodingKeys.self)
  182. guard let onlyKey = container.allKeys.first, container.allKeys.count == 1 else {
  183. throw DecodingError
  184. .typeMismatch(
  185. Self<Message,
  186. Result>.self,
  187. DecodingError.Context(
  188. codingPath: container.codingPath,
  189. debugDescription: "Invalid number of keys found, expected one.",
  190. underlyingError: nil
  191. )
  192. )
  193. }
  194. switch onlyKey {
  195. case .message:
  196. self = try Self
  197. .message(container.decode(Message.self, forKey: .message))
  198. case .result:
  199. self = try Self
  200. .result(container.decode(Result.self, forKey: .result))
  201. }
  202. } catch {
  203. throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
  204. }
  205. }
  206. }
  207. @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
  208. public extension Callable where Request: Sendable, Response: Sendable {
  209. /// Creates a stream that yields responses from the streaming callable function.
  210. ///
  211. /// The request to the Cloud Functions backend made by this method automatically includes a FCM
  212. /// token to identify the app instance. If a user is logged in with Firebase Auth, an auth ID
  213. /// token for the user is included. If App Check is integrated, an app check token is included.
  214. ///
  215. /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
  216. /// information regarding the app instance. To stop this, see `Messaging.deleteData()`. It
  217. /// resumes with a new FCM Token the next time you call this method.
  218. ///
  219. /// - Important: The final result returned by the callable function is only accessible when
  220. /// using `StreamResponse` as the `Response` generic type.
  221. ///
  222. /// Example of using `stream` _without_ `StreamResponse`:
  223. /// ```swift
  224. /// let callable: Callable<MyRequest, MyResponse> = // ...
  225. /// let request: MyRequest = // ...
  226. /// let stream = try callable.stream(request)
  227. /// for try await response in stream {
  228. /// // Process each `MyResponse` message
  229. /// print(response)
  230. /// }
  231. /// ```
  232. ///
  233. /// Example of using `stream` _with_ `StreamResponse`:
  234. /// ```swift
  235. /// let callable: Callable<MyRequest, StreamResponse<MyMessage, MyResult>> = // ...
  236. /// let request: MyRequest = // ...
  237. /// let stream = try callable.stream(request)
  238. /// for try await response in stream {
  239. /// switch response {
  240. /// case .message(let message):
  241. /// // Process each `MyMessage`
  242. /// print(message)
  243. /// case .result(let result):
  244. /// // Process the final `MyResult`
  245. /// print(result)
  246. /// }
  247. /// }
  248. /// ```
  249. ///
  250. /// - Parameter data: The `Request` data to pass to the callable function.
  251. /// - Throws: A ``FunctionsError`` if the parameter `data` cannot be encoded.
  252. /// - Returns: A stream wrapping responses yielded by the streaming callable function or
  253. /// a ``FunctionsError`` if an error occurred.
  254. func stream(_ data: Request? = nil) throws -> AsyncThrowingStream<Response, Error> {
  255. let encoded: SendableWrapper
  256. do {
  257. encoded = try SendableWrapper(value: encoder.encode(data))
  258. } catch {
  259. throw FunctionsError(.invalidArgument, userInfo: [NSUnderlyingErrorKey: error])
  260. }
  261. return AsyncThrowingStream { continuation in
  262. Task {
  263. do {
  264. for try await response in callable.stream(encoded) {
  265. do {
  266. // This response JSON should only be able to be decoded to an `StreamResponse<_, _>`
  267. // instance. If the decoding succeeds and the decoded response conforms to
  268. // `StreamResponseProtocol`, we know the `Response` generic argument
  269. // is `StreamResponse<_, _>`.
  270. let responseJSON = switch response {
  271. case let .message(json), let .result(json): json
  272. }
  273. let response = try decoder.decode(Response.self, from: responseJSON)
  274. if response is StreamResponseProtocol {
  275. continuation.yield(response)
  276. } else {
  277. // `Response` is a custom type that matched the decoding logic as the
  278. // `StreamResponse<_, _>` type. Only the `StreamResponse<_, _>` type should decode
  279. // successfully here to avoid exposing the `result` value in a custom type.
  280. throw FunctionsError(.internal)
  281. }
  282. } catch let error as FunctionsError where error.code == .dataLoss {
  283. // `Response` is of type `StreamResponse<_, _>`, but failed to decode. Rethrow.
  284. throw error
  285. } catch {
  286. // `Response` is *not* of type `StreamResponse<_, _>`, and needs to be unboxed and
  287. // decoded.
  288. guard case let .message(messageJSON) = response else {
  289. // Since `Response` is not a `StreamResponse<_, _>`, only messages should be
  290. // decoded.
  291. continue
  292. }
  293. do {
  294. let boxedMessage = try decoder.decode(
  295. StreamResponseMessage.self,
  296. from: messageJSON
  297. )
  298. continuation.yield(boxedMessage.message)
  299. } catch {
  300. throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
  301. }
  302. }
  303. }
  304. } catch {
  305. continuation.finish(throwing: error)
  306. }
  307. continuation.finish()
  308. }
  309. }
  310. }
  311. /// A container type for the type-safe decoding of the message object from the generic `Response`
  312. /// type.
  313. private struct StreamResponseMessage: Decodable {
  314. let message: Response
  315. }
  316. }
  317. /// A container type for differentiating between message and result responses.
  318. enum JSONStreamResponse {
  319. case message([String: Any])
  320. case result([String: Any])
  321. }
  322. // TODO(Swift 6): Remove need for below type by changing `FirebaseDataEncoder` to not returning
  323. // `Any`.
  324. /// This wrapper is only intended to be used for passing encoded data in the
  325. /// `stream` function's hierarchy. When using, carefully audit that `value` is
  326. /// only ever accessed in one isolation domain.
  327. struct SendableWrapper: @unchecked Sendable {
  328. let value: Any
  329. }