Callable+Codable.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. 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> {
  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 (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. completion(.failure(error))
  78. }
  79. }
  80. /// Creates a directly callable function.
  81. ///
  82. /// This allows users to call a HTTPS Callable Function like a normal Swift function:
  83. /// ```swift
  84. /// let greeter = functions.httpsCallable("greeter",
  85. /// requestType: GreetingRequest.self,
  86. /// responseType: GreetingResponse.self)
  87. /// greeter(data) { result in
  88. /// print(result.greeting)
  89. /// }
  90. /// ```
  91. /// You can also call a HTTPS Callable function using the following syntax:
  92. /// ```swift
  93. /// let greeter: Callable<GreetingRequest, GreetingResponse> =
  94. /// functions.httpsCallable("greeter")
  95. /// greeter(data) { result in
  96. /// print(result.greeting)
  97. /// }
  98. /// ```
  99. /// - Parameters:
  100. /// - data: Parameters to pass to the trigger.
  101. /// - completion: The block to call when the HTTPS request has completed.
  102. public func callAsFunction(_ data: Request,
  103. completion: @escaping (Result<Response, Error>)
  104. -> Void) {
  105. call(data, completion: completion)
  106. }
  107. /// Executes this Callable HTTPS trigger asynchronously.
  108. ///
  109. /// The data passed into the trigger must be of the generic `Request` type:
  110. ///
  111. /// The request to the Cloud Functions backend made by this method automatically includes a
  112. /// FCM token to identify the app instance. If a user is logged in with Firebase
  113. /// Auth, an auth ID token for the user is also automatically included.
  114. ///
  115. /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
  116. /// information
  117. /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
  118. /// resumes with a new FCM Token the next time you call this method.
  119. ///
  120. /// - Parameter data: The `Request` representing the data to pass to the trigger.
  121. ///
  122. /// - Throws: An error if any value throws an error during encoding or decoding.
  123. /// - Throws: An error if the callable fails to complete
  124. ///
  125. /// - Returns: The decoded `Response` value
  126. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  127. public func call(_ data: Request) async throws -> Response {
  128. let encoded = try encoder.encode(data)
  129. let result = try await callable.call(encoded)
  130. return try decoder.decode(Response.self, from: result.data)
  131. }
  132. /// Creates a directly callable function.
  133. ///
  134. /// This allows users to call a HTTPS Callable Function like a normal Swift function:
  135. /// ```swift
  136. /// let greeter = functions.httpsCallable("greeter",
  137. /// requestType: GreetingRequest.self,
  138. /// responseType: GreetingResponse.self)
  139. /// let result = try await greeter(data)
  140. /// print(result.greeting)
  141. /// ```
  142. /// You can also call a HTTPS Callable function using the following syntax:
  143. /// ```swift
  144. /// let greeter: Callable<GreetingRequest, GreetingResponse> =
  145. /// functions.httpsCallable("greeter")
  146. /// let result = try await greeter(data)
  147. /// print(result.greeting)
  148. /// ```
  149. /// - Parameters:
  150. /// - data: Parameters to pass to the trigger.
  151. /// - Returns: The decoded `Response` value
  152. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  153. public func callAsFunction(_ data: Request) async throws -> Response {
  154. return try await call(data)
  155. }
  156. }
  157. /// Used to determine when a `StreamResponse<_, _>` is being decoded.
  158. private protocol StreamResponseProtocol {}
  159. /// A convenience type used to receive both the streaming callable function's yielded messages and
  160. /// its return value.
  161. ///
  162. /// This can be used as the generic `Response` parameter to ``Callable`` to receive both the
  163. /// yielded messages and final return value of the streaming callable function.
  164. @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
  165. public enum StreamResponse<Message: Decodable, Result: Decodable>: Decodable,
  166. StreamResponseProtocol {
  167. /// The message yielded by the callable function.
  168. case message(Message)
  169. /// The final result returned by the callable function.
  170. case result(Result)
  171. private enum CodingKeys: String, CodingKey {
  172. case message
  173. case result
  174. }
  175. public init(from decoder: any Decoder) throws {
  176. do {
  177. let container = try decoder
  178. .container(keyedBy: Self<Message, Result>.CodingKeys.self)
  179. guard let onlyKey = container.allKeys.first, container.allKeys.count == 1 else {
  180. throw DecodingError
  181. .typeMismatch(
  182. Self<Message,
  183. Result>.self,
  184. DecodingError.Context(
  185. codingPath: container.codingPath,
  186. debugDescription: "Invalid number of keys found, expected one.",
  187. underlyingError: nil
  188. )
  189. )
  190. }
  191. switch onlyKey {
  192. case .message:
  193. self = try Self
  194. .message(container.decode(Message.self, forKey: .message))
  195. case .result:
  196. self = try Self
  197. .result(container.decode(Result.self, forKey: .result))
  198. }
  199. } catch {
  200. throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
  201. }
  202. }
  203. }
  204. @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
  205. public extension Callable where Request: Sendable, Response: Sendable {
  206. /// Creates a stream that yields responses from the streaming callable function.
  207. ///
  208. /// The request to the Cloud Functions backend made by this method automatically includes a FCM
  209. /// token to identify the app instance. If a user is logged in with Firebase Auth, an auth ID
  210. /// token for the user is included. If App Check is integrated, an app check token is included.
  211. ///
  212. /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
  213. /// information regarding the app instance. To stop this, see `Messaging.deleteData()`. It
  214. /// resumes with a new FCM Token the next time you call this method.
  215. ///
  216. /// - Important: The final result returned by the callable function is only accessible when
  217. /// using `StreamResponse` as the `Response` generic type.
  218. ///
  219. /// Example of using `stream` _without_ `StreamResponse`:
  220. /// ```swift
  221. /// let callable: Callable<MyRequest, MyResponse> = // ...
  222. /// let request: MyRequest = // ...
  223. /// let stream = try callable.stream(request)
  224. /// for try await response in stream {
  225. /// // Process each `MyResponse` message
  226. /// print(response)
  227. /// }
  228. /// ```
  229. ///
  230. /// Example of using `stream` _with_ `StreamResponse`:
  231. /// ```swift
  232. /// let callable: Callable<MyRequest, StreamResponse<MyMessage, MyResult>> = // ...
  233. /// let request: MyRequest = // ...
  234. /// let stream = try callable.stream(request)
  235. /// for try await response in stream {
  236. /// switch response {
  237. /// case .message(let message):
  238. /// // Process each `MyMessage`
  239. /// print(message)
  240. /// case .result(let result):
  241. /// // Process the final `MyResult`
  242. /// print(result)
  243. /// }
  244. /// }
  245. /// ```
  246. ///
  247. /// - Parameter data: The `Request` data to pass to the callable function.
  248. /// - Throws: A ``FunctionsError`` if the parameter `data` cannot be encoded.
  249. /// - Returns: A stream wrapping responses yielded by the streaming callable function or
  250. /// a ``FunctionsError`` if an error occurred.
  251. func stream(_ data: Request? = nil) throws -> AsyncThrowingStream<Response, Error> {
  252. let encoded: Any
  253. do {
  254. encoded = try encoder.encode(data)
  255. } catch {
  256. throw FunctionsError(.invalidArgument, userInfo: [NSUnderlyingErrorKey: error])
  257. }
  258. return AsyncThrowingStream { continuation in
  259. Task {
  260. do {
  261. for try await response in callable.stream(encoded) {
  262. do {
  263. // This response JSON should only be able to be decoded to an `StreamResponse<_, _>`
  264. // instance. If the decoding succeeds and the decoded response conforms to
  265. // `StreamResponseProtocol`, we know the `Response` generic argument
  266. // is `StreamResponse<_, _>`.
  267. let responseJSON = switch response {
  268. case .message(let json), .result(let json): json
  269. }
  270. let response = try decoder.decode(Response.self, from: responseJSON)
  271. if response is StreamResponseProtocol {
  272. continuation.yield(response)
  273. } else {
  274. // `Response` is a custom type that matched the decoding logic as the
  275. // `StreamResponse<_, _>` type. Only the `StreamResponse<_, _>` type should decode
  276. // successfully here to avoid exposing the `result` value in a custom type.
  277. throw FunctionsError(.internal)
  278. }
  279. } catch let error as FunctionsError where error.code == .dataLoss {
  280. // `Response` is of type `StreamResponse<_, _>`, but failed to decode. Rethrow.
  281. throw error
  282. } catch {
  283. // `Response` is *not* of type `StreamResponse<_, _>`, and needs to be unboxed and
  284. // decoded.
  285. guard case let .message(messageJSON) = response else {
  286. // Since `Response` is not a `StreamResponse<_, _>`, only messages should be
  287. // decoded.
  288. continue
  289. }
  290. do {
  291. let boxedMessage = try decoder.decode(
  292. StreamResponseMessage.self,
  293. from: messageJSON
  294. )
  295. continuation.yield(boxedMessage.message)
  296. } catch {
  297. throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
  298. }
  299. }
  300. }
  301. } catch {
  302. continuation.finish(throwing: error)
  303. }
  304. continuation.finish()
  305. }
  306. }
  307. }
  308. /// A container type for the type-safe decoding of the message object from the generic `Response`
  309. /// type.
  310. private struct StreamResponseMessage: Decodable {
  311. let message: Response
  312. }
  313. }
  314. /// A container type for differentiating between message and result responses.
  315. enum JSONStreamResponse {
  316. case message([String: Any])
  317. case result([String: Any])
  318. }