Skip to content

Commit f1cef07

Browse files
ryanwilsonpaulb777
authored andcommitted
It builds
1 parent 64c81c0 commit f1cef07

File tree

8 files changed

+981
-20
lines changed

8 files changed

+981
-20
lines changed

FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,8 @@
1313
// limitations under the License.
1414

1515
import Foundation
16-
import FirebaseFunctions
1716
import FirebaseSharedSwift
1817

19-
public extension Functions {
20-
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
21-
/// request and the type of a `Decodable` response.
22-
/// - Parameter name: The name of the Callable HTTPS trigger
23-
/// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
24-
/// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
25-
/// - Parameter encoder: The encoder instance to use to run the encoding.
26-
/// - Parameter decoder: The decoder instance to use to run the decoding.
27-
func httpsCallable<Request: Encodable,
28-
Response: Decodable>(_ name: String,
29-
requestAs: Request.Type = Request.self,
30-
responseAs: Response.Type = Response.self,
31-
encoder: FirebaseDataEncoder = FirebaseDataEncoder(),
32-
decoder: FirebaseDataDecoder = FirebaseDataDecoder())
33-
-> Callable<Request, Response> {
34-
return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder)
35-
}
36-
}
37-
3818
// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions.
3919
public struct Callable<Request: Encodable, Response: Decodable> {
4020
/// The timeout to use when calling the function. Defaults to 60 seconds.
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Ryan Wilson on 2022-01-25.
6+
//
7+
8+
import Foundation
9+
import FirebaseCore
10+
import FirebaseSharedSwift
11+
import GTMSessionFetcherCore
12+
13+
// PLACEHOLDERS
14+
protocol AuthInterop {
15+
func getToken(forcingRefresh: Bool, callback: (Result<String, Error>) -> Void)
16+
}
17+
protocol MessagingInterop {
18+
var fcmToken: String { get }
19+
}
20+
protocol AppCheckInterop {
21+
func getToken(forcingRefresh: Bool, callback: (Result<String, Error>) -> Void)
22+
}
23+
24+
// END PLACEHOLDERS
25+
26+
private enum Constants {
27+
static let appCheckTokenHeader = "X-Firebase-AppCheck"
28+
static let fcmTokenHeader = "Firebase-Instance-ID-Token"
29+
}
30+
31+
/**
32+
* `Functions` is the client for Cloud Functions for a Firebase project.
33+
*/
34+
@objc public class Functions : NSObject {
35+
36+
// MARK: - Private Variables
37+
38+
/// The network client to use for http requests.
39+
private let fetcherService: GTMSessionFetcherService
40+
// The projectID to use for all function references.
41+
private let projectID: String
42+
// The region to use for all function references.
43+
private let region: String
44+
// The custom domain to use for all functions references (optional).
45+
private let customDomain: String?
46+
// A serializer to encode/decode data and return values.
47+
private let serializer = FUNSerializer()
48+
// A factory for getting the metadata to include with function calls.
49+
private let contextProvider: FunctionsContextProvider
50+
51+
/**
52+
* The current emulator origin, or nil if it is not set.
53+
*/
54+
private(set) var emulatorOrigin: String?
55+
56+
/**
57+
* Creates a Cloud Functions client with the given app and region, or returns a pre-existing
58+
* instance if one already exists.
59+
* @param app The app for the Firebase project.
60+
* @param region The region for the http trigger, such as "us-central1".
61+
*/
62+
class func functions(app: FirebaseApp = FirebaseApp.app()!, region: String = "us-central1") -> Functions {
63+
return Functions(app: app, region: region, customDomain: nil)
64+
}
65+
66+
/**
67+
* Creates a Cloud Functions client with the given app and region, or returns a pre-existing
68+
* instance if one already exists.
69+
* @param app The app for the Firebase project.
70+
* @param customDomain A custom domain for the http trigger, such as "https://mydomain.com".
71+
*/
72+
class func functions(app: FirebaseApp = FirebaseApp.app()!, customDomain: String) -> Functions {
73+
return Functions(app: app, region: "us-central1", customDomain: customDomain)
74+
}
75+
76+
internal convenience init(app: FirebaseApp,
77+
region: String,
78+
customDomain: String?) {
79+
#warning("Should be fetched from the App's component container instead.")
80+
/*
81+
id<FIRFunctionsProvider> provider = FIR_COMPONENT(FIRFunctionsProvider, app.container);
82+
return [provider functionsForApp:app region:region customDomain:customDomain type:[self class]];
83+
*/
84+
self.init(projectID: app.options.projectID!,
85+
region: region,
86+
customDomain: customDomain,
87+
// TODO: Get this out of the app.
88+
auth: nil,
89+
messaging: nil,
90+
appCheck: nil)
91+
}
92+
93+
private init(projectID: String,
94+
region: String,
95+
customDomain: String?,
96+
auth: AuthInterop?,
97+
messaging: MessagingInterop?,
98+
appCheck: AppCheckInterop?,
99+
fetcherService: GTMSessionFetcherService = GTMSessionFetcherService()) {
100+
self.projectID = projectID
101+
self.region = region
102+
self.customDomain = customDomain
103+
self.emulatorOrigin = nil
104+
self.contextProvider = FunctionsContextProvider(auth: auth,
105+
messaging: messaging,
106+
appCheck: appCheck)
107+
self.fetcherService = fetcherService
108+
}
109+
110+
/**
111+
* Creates a reference to the Callable HTTPS trigger with the given name.
112+
* @param name The name of the Callable HTTPS trigger.
113+
*/
114+
func httpsCallable(_ name: String) -> HTTPSCallable {
115+
return HTTPSCallable(functions: self, name: name)
116+
}
117+
118+
/**
119+
* Changes this instance to point to a Cloud Functions emulator running locally.
120+
* See https://firebase.google.com/docs/functions/local-emulator
121+
* @param host The host of the local emulator, such as "localhost".
122+
* @param port The port of the local emulator, for example 5005.
123+
*/
124+
func useEmulator(withHost host: String, port: Int) {
125+
let prefix = host.hasPrefix("http") ? "" : "http://"
126+
let origin = String(format: "\(prefix)\(host):%li", port)
127+
emulatorOrigin = origin
128+
}
129+
130+
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
131+
/// request and the type of a `Decodable` response.
132+
/// - Parameter name: The name of the Callable HTTPS trigger
133+
/// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
134+
/// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
135+
/// - Parameter encoder: The encoder instance to use to run the encoding.
136+
/// - Parameter decoder: The decoder instance to use to run the decoding.
137+
func httpsCallable<Request: Encodable,
138+
Response: Decodable>(_ name: String,
139+
requestAs: Request.Type = Request.self,
140+
responseAs: Response.Type = Response.self,
141+
encoder: FirebaseDataEncoder = FirebaseDataEncoder(),
142+
decoder: FirebaseDataDecoder = FirebaseDataDecoder())
143+
-> Callable<Request, Response> {
144+
return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder)
145+
}
146+
147+
// MARK: - Private Funcs
148+
149+
private func urlWithName(_ name: String) -> String {
150+
assert(!name.isEmpty, "Name cannot be empty")
151+
152+
// Check if we're using the emulator
153+
if let emulatorOrigin = emulatorOrigin {
154+
return "\(emulatorOrigin)/\(projectID)/\(region)/\(name)"
155+
}
156+
157+
// Check the custom domain.
158+
if let customDomain = customDomain {
159+
return "\(customDomain)/\(name)"
160+
}
161+
162+
return "https://\(region)-\(projectID).cloudfunctions.net/\(name)"
163+
}
164+
165+
internal func callFunction(name: String,
166+
withObject data: Any?,
167+
timeout: TimeInterval,
168+
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
169+
// Get context first.
170+
contextProvider.getContext { context, error in
171+
// Note: context is always non-nil since some checks could succeed, we're only failing if
172+
// there's an error.
173+
if let error = error {
174+
completion(.failure(error))
175+
} else {
176+
self.callFunction(name: name,
177+
withObject: data,
178+
timeout: timeout,
179+
context: context,
180+
completion: completion)
181+
}
182+
}
183+
184+
}
185+
186+
private func callFunction(name: String,
187+
withObject data: Any?,
188+
timeout: TimeInterval,
189+
context: FunctionsContext,
190+
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
191+
let url = URL(string: urlWithName(name))!
192+
let request = URLRequest(url: url,
193+
cachePolicy: .useProtocolCachePolicy,
194+
timeoutInterval: timeout)
195+
let fetcher = fetcherService.fetcher(with: request)
196+
let body = NSMutableDictionary()
197+
198+
// Encode the data in the body.
199+
var localData = data
200+
if data == nil {
201+
localData = NSNull()
202+
}
203+
// Force unwrap to match the old invalid argument thrown.
204+
let encoded = try! serializer.encode(localData!)
205+
body["data"] = encoded
206+
207+
do {
208+
let payload = try JSONSerialization.data(withJSONObject: body)
209+
fetcher.bodyData = payload
210+
} catch {
211+
DispatchQueue.main.async {
212+
completion(.failure(error))
213+
}
214+
return
215+
}
216+
217+
// Set the headers.
218+
fetcher.setRequestValue("application/json", forHTTPHeaderField: "Content-Type")
219+
if let authToken = context.authToken {
220+
let value = "Bearer \(authToken)"
221+
fetcher.setRequestValue(value, forHTTPHeaderField: "Authorization")
222+
}
223+
224+
if let fcmToken = context.fcmToken {
225+
fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader)
226+
}
227+
228+
if let appCheckToken = context.appCheckToken {
229+
fetcher.setRequestValue(appCheckToken, forHTTPHeaderField: Constants.appCheckTokenHeader)
230+
}
231+
232+
// Override normal security rules if this is a local test.
233+
if emulatorOrigin != nil {
234+
fetcher.allowLocalhostRequest = true
235+
fetcher.allowedInsecureSchemes = ["http"]
236+
}
237+
238+
fetcher.beginFetch { data, error in
239+
// If there was an HTTP error, convert it to our own error domain.
240+
var localError: Error? = nil
241+
if let error = error as NSError? {
242+
if error.domain == kGTMSessionFetcherStatusDomain {
243+
localError = FunctionsErrorForResponse(status: error.code, body: data, serializer: self.serializer)
244+
} else if error.domain == NSURLErrorDomain, error.code == NSURLErrorTimedOut {
245+
localError = FunctionsErrorCode.deadlineExceeded.generatedError(userInfo: nil)
246+
}
247+
} else {
248+
// If there wasn't an HTTP error, see if there was an error in the body.
249+
localError = FunctionsErrorForResponse(status: 200, body: data, serializer: self.serializer)
250+
}
251+
252+
// If there was an error, report it to the user and stop.
253+
if let localError = localError {
254+
completion(.failure(localError))
255+
return
256+
}
257+
258+
// Porting: this check is new since we didn't previously check if `data` was nil.
259+
guard let data = data else {
260+
completion(.failure(FunctionsErrorCode.internal.generatedError(userInfo: nil)))
261+
return
262+
}
263+
264+
let responseJSONObject: Any
265+
do {
266+
responseJSONObject = try JSONSerialization.jsonObject(with: data)
267+
} catch {
268+
completion(.failure(error))
269+
return
270+
}
271+
272+
guard let responseJSON = responseJSONObject as? NSDictionary else {
273+
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
274+
completion(.failure(FunctionsErrorCode.internal.generatedError(userInfo: userInfo)))
275+
return
276+
}
277+
278+
// TODO(klimt): Allow "result" instead of "data" for now, for backwards compatibility.
279+
let dataJSON = responseJSON["data"] ?? responseJSON["result"]
280+
guard let dataJSON = dataJSON as AnyObject? else {
281+
let userInfo = [NSLocalizedDescriptionKey: "Response is missing data field."]
282+
completion(.failure(FunctionsErrorCode.internal.generatedError(userInfo: userInfo)))
283+
return
284+
}
285+
286+
let resultData: Any?
287+
do {
288+
resultData = try self.serializer.decode(dataJSON)
289+
} catch {
290+
completion(.failure(error))
291+
return
292+
}
293+
294+
// TODO: Force unwrap... gross
295+
let result = HTTPSCallableResult(data: resultData!)
296+
#warning("This copied comment appears to be incorrect - it's impossible to have a nil callable result.")
297+
// If there's no result field, this will return nil, which is fine.
298+
completion(.success(result))
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)