Skip to content

Commit dbecea0

Browse files
authored
[Functions] Complete Swift 6 support (#14838)
1 parent 122f2d9 commit dbecea0

File tree

7 files changed

+87
-33
lines changed

7 files changed

+87
-33
lines changed

.github/workflows/functions.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
strategy:
3232
matrix:
3333
target: [ios, tvos, macos, watchos]
34+
swift_version: [5.9, 6.0]
3435
build-env:
3536
- os: macos-15
3637
xcode: Xcode_16.3
@@ -44,6 +45,8 @@ jobs:
4445
run: scripts/setup_bundler.sh
4546
- name: Integration Test Server
4647
run: FirebaseFunctions/Backend/start.sh synchronous
48+
- name: Set Swift swift_version
49+
run: sed -i "" "s/s.swift_version[[:space:]]*=[[:space:]]*'5.9'/s.swift_version = '${{ matrix.swift_version }}'/" FirebaseFunctions.podspec
4750
- name: Build and test
4851
run: |
4952
scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseFunctions.podspec \

FirebaseFunctions/Sources/Callable+Codable.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ private protocol StreamResponseProtocol {}
175175
/// This can be used as the generic `Response` parameter to ``Callable`` to receive both the
176176
/// yielded messages and final return value of the streaming callable function.
177177
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
178-
public enum StreamResponse<Message: Decodable, Result: Decodable>: Decodable,
178+
public enum StreamResponse<Message: Decodable & Sendable, Result: Decodable & Sendable>: Decodable,
179+
Sendable,
179180
StreamResponseProtocol {
180181
/// The message yielded by the callable function.
181182
case message(Message)

FirebaseFunctions/Sources/FunctionsError.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,10 @@ private extension FunctionsErrorCode {
151151
}
152152
}
153153

154+
// TODO(ncooke3): Revisit this unchecked Sendable conformance.
155+
154156
/// The object used to report errors that occur during a function’s execution.
155-
struct FunctionsError: CustomNSError {
157+
struct FunctionsError: CustomNSError, @unchecked Sendable {
156158
static let errorDomain = FunctionsErrorDomain
157159

158160
let code: FunctionsErrorCode

FirebaseFunctions/Sources/HTTPSCallable.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,39 @@ open class HTTPSCallable: NSObject, @unchecked Sendable {
7171
/// - Parameters:
7272
/// - data: Parameters to pass to the trigger.
7373
/// - completion: The block to call when the HTTPS request has completed.
74+
@available(swift 1000.0) // Objective-C only API
7475
@objc(callWithObject:completion:) open func call(_ data: Any? = nil,
7576
completion: @escaping @MainActor (HTTPSCallableResult?,
7677
Error?)
7778
-> Void) {
79+
sendableCallable.call(SendableWrapper(value: data as Any), completion: completion)
80+
}
81+
82+
/// Executes this Callable HTTPS trigger asynchronously.
83+
///
84+
/// The data passed into the trigger can be any of the following types:
85+
/// - `nil` or `NSNull`
86+
/// - `String`
87+
/// - `NSNumber`, or any Swift numeric type bridgeable to `NSNumber`
88+
/// - `[Any]`, where the contained objects are also one of these types.
89+
/// - `[String: Any]` where the values are also one of these types.
90+
///
91+
/// The request to the Cloud Functions backend made by this method automatically includes a
92+
/// Firebase Installations ID token to identify the app instance. If a user is logged in with
93+
/// Firebase Auth, an auth ID token for the user is also automatically included.
94+
///
95+
/// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect
96+
/// information
97+
/// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
98+
/// resumes with a new FCM Token the next time you call this method.
99+
///
100+
/// - Parameters:
101+
/// - data: Parameters to pass to the trigger.
102+
/// - completion: The block to call when the HTTPS request has completed.
103+
@nonobjc open func call(_ data: sending Any? = nil,
104+
completion: @escaping @MainActor (HTTPSCallableResult?,
105+
Error?)
106+
-> Void) {
78107
sendableCallable.call(data, completion: completion)
79108
}
80109

@@ -154,6 +183,7 @@ private extension HTTPSCallable {
154183

155184
func call(_ data: sending Any? = nil,
156185
completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) {
186+
let data = (data as? SendableWrapper)?.value ?? data
157187
if #available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) {
158188
Task {
159189
do {

FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ extension FunctionsSerializer {
3131
final class FunctionsSerializer: Sendable {
3232
// MARK: - Internal APIs
3333

34+
// This function only supports the following types and will otherwise throw
35+
// an error.
36+
// - NSNull (note: `nil` collection values from a Swift caller will be treated as NSNull)
37+
// - NSNumber
38+
// - NSString
39+
// - NSDicionary
40+
// - NSArray
3441
func encode(_ object: Any) throws -> Any {
3542
if object is NSNull {
3643
return object
@@ -53,6 +60,13 @@ final class FunctionsSerializer: Sendable {
5360
}
5461
}
5562

63+
// This function only supports the following types and will otherwise throw
64+
// an error.
65+
// - NSNull (note: `nil` collection values from a Swift caller will be treated as NSNull)
66+
// - NSNumber
67+
// - NSString
68+
// - NSDicionary
69+
// - NSArray
5670
func decode(_ object: Any) throws -> Any {
5771
// Return these types as is. PORTING NOTE: Moved from the bottom of the func for readability.
5872
if let dict = object as? NSDictionary {

FirebaseFunctions/Tests/Integration/IntegrationTests.swift

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class IntegrationTests: XCTestCase {
8383
return URL(string: "http://localhost:5005/functions-integration-test/us-central1/\(funcName)")!
8484
}
8585

86-
func testData() {
86+
@MainActor func testData() {
8787
let data = DataTestRequest(
8888
bool: true,
8989
int: 2,
@@ -148,7 +148,7 @@ class IntegrationTests: XCTestCase {
148148
}
149149
}
150150

151-
func testScalar() {
151+
@MainActor func testScalar() {
152152
let byName = functions.httpsCallable(
153153
"scalarTest",
154154
requestAs: Int16.self,
@@ -203,7 +203,7 @@ class IntegrationTests: XCTestCase {
203203
}
204204
}
205205

206-
func testToken() {
206+
@MainActor func testToken() {
207207
// Recreate functions with a token.
208208
let functions = Functions(
209209
projectID: "functions-integration-test",
@@ -271,7 +271,7 @@ class IntegrationTests: XCTestCase {
271271
}
272272
}
273273

274-
func testFCMToken() {
274+
@MainActor func testFCMToken() {
275275
let byName = functions.httpsCallable(
276276
"FCMTokenTest",
277277
requestAs: [String: Int].self,
@@ -316,7 +316,7 @@ class IntegrationTests: XCTestCase {
316316
}
317317
}
318318

319-
func testNull() {
319+
@MainActor func testNull() {
320320
let byName = functions.httpsCallable(
321321
"nullTest",
322322
requestAs: Int?.self,
@@ -361,7 +361,7 @@ class IntegrationTests: XCTestCase {
361361
}
362362
}
363363

364-
func testMissingResult() {
364+
@MainActor func testMissingResult() {
365365
let byName = functions.httpsCallable(
366366
"missingResultTest",
367367
requestAs: Int?.self,
@@ -415,7 +415,7 @@ class IntegrationTests: XCTestCase {
415415
}
416416
}
417417

418-
func testUnhandledError() {
418+
@MainActor func testUnhandledError() {
419419
let byName = functions.httpsCallable(
420420
"unhandledErrorTest",
421421
requestAs: [Int].self,
@@ -469,7 +469,7 @@ class IntegrationTests: XCTestCase {
469469
}
470470
}
471471

472-
func testUnknownError() {
472+
@MainActor func testUnknownError() {
473473
let byName = functions.httpsCallable(
474474
"unknownErrorTest",
475475
requestAs: [Int].self,
@@ -522,7 +522,7 @@ class IntegrationTests: XCTestCase {
522522
}
523523
}
524524

525-
func testExplicitError() {
525+
@MainActor func testExplicitError() {
526526
let byName = functions.httpsCallable(
527527
"explicitErrorTest",
528528
requestAs: [Int].self,
@@ -579,7 +579,7 @@ class IntegrationTests: XCTestCase {
579579
}
580580
}
581581

582-
func testHttpError() {
582+
@MainActor func testHttpError() {
583583
let byName = functions.httpsCallable(
584584
"httpErrorTest",
585585
requestAs: [Int].self,
@@ -631,7 +631,7 @@ class IntegrationTests: XCTestCase {
631631
}
632632
}
633633

634-
func testThrowError() {
634+
@MainActor func testThrowError() {
635635
let byName = functions.httpsCallable(
636636
"throwTest",
637637
requestAs: [Int].self,
@@ -685,7 +685,7 @@ class IntegrationTests: XCTestCase {
685685
}
686686
}
687687

688-
func testTimeout() {
688+
@MainActor func testTimeout() {
689689
let byName = functions.httpsCallable(
690690
"timeoutTest",
691691
requestAs: [Int].self,
@@ -743,7 +743,7 @@ class IntegrationTests: XCTestCase {
743743
}
744744
}
745745

746-
func testCallAsFunction() {
746+
@MainActor func testCallAsFunction() {
747747
let data = DataTestRequest(
748748
bool: true,
749749
int: 2,
@@ -808,7 +808,7 @@ class IntegrationTests: XCTestCase {
808808
}
809809
}
810810

811-
func testInferredTypes() {
811+
@MainActor func testInferredTypes() {
812812
let data = DataTestRequest(
813813
bool: true,
814814
int: 2,
@@ -868,7 +868,7 @@ class IntegrationTests: XCTestCase {
868868
}
869869
}
870870

871-
func testFunctionsReturnsOnMainThread() {
871+
@MainActor func testFunctionsReturnsOnMainThread() {
872872
let expectation = expectation(description: #function)
873873
functions.httpsCallable(
874874
"scalarTest",
@@ -884,7 +884,7 @@ class IntegrationTests: XCTestCase {
884884
waitForExpectations(timeout: 5)
885885
}
886886

887-
func testFunctionsThrowsOnMainThread() {
887+
@MainActor func testFunctionsThrowsOnMainThread() {
888888
let expectation = expectation(description: #function)
889889
functions.httpsCallable(
890890
"httpErrorTest",
@@ -908,7 +908,7 @@ class IntegrationTests: XCTestCase {
908908
///
909909
/// This can be used as the generic `Request` parameter to ``Callable`` to
910910
/// indicate the callable function does not accept parameters.
911-
private struct EmptyRequest: Encodable {}
911+
private struct EmptyRequest: Encodable, Sendable {}
912912

913913
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
914914
extension IntegrationTests {
@@ -1100,18 +1100,21 @@ extension IntegrationTests {
11001100
)
11011101
}
11021102

1103-
func testStream_Canceled() async throws {
1104-
let task = Task.detached { [self] in
1105-
let callable: Callable<EmptyRequest, String> = functions.httpsCallable("genStream")
1106-
let stream = try callable.stream()
1107-
// Since we cancel the call we are expecting an empty array.
1108-
return try await stream.reduce([]) { $0 + [$1] } as [String]
1103+
// Concurrency rules prevent easily testing this feature.
1104+
#if swift(<6)
1105+
func testStream_Canceled() async throws {
1106+
let task = Task.detached { [self] in
1107+
let callable: Callable<EmptyRequest, String> = functions.httpsCallable("genStream")
1108+
let stream = try callable.stream()
1109+
// Since we cancel the call we are expecting an empty array.
1110+
return try await stream.reduce([]) { $0 + [$1] } as [String]
1111+
}
1112+
// We cancel the task and we expect a null response even if the stream was initiated.
1113+
task.cancel()
1114+
let respone = try await task.value
1115+
XCTAssertEqual(respone, [])
11091116
}
1110-
// We cancel the task and we expect a null response even if the stream was initiated.
1111-
task.cancel()
1112-
let respone = try await task.value
1113-
XCTAssertEqual(respone, [])
1114-
}
1117+
#endif
11151118

11161119
func testStream_NonexistentFunction() async throws {
11171120
let callable: Callable<EmptyRequest, String> = functions.httpsCallable(
@@ -1163,7 +1166,8 @@ extension IntegrationTests {
11631166
func testStream_ResultIsOnlyExposedInStreamResponse() async throws {
11641167
// The implementation is copied from `StreamResponse`. The only difference is the do-catch is
11651168
// removed from the decoding initializer.
1166-
enum MyStreamResponse<Message: Decodable, Result: Decodable>: Decodable {
1169+
enum MyStreamResponse<Message: Decodable & Sendable, Result: Decodable & Sendable>: Decodable,
1170+
Sendable {
11671171
/// The message yielded by the callable function.
11681172
case message(Message)
11691173
/// The final result returned by the callable function.
@@ -1248,7 +1252,7 @@ extension IntegrationTests {
12481252
}
12491253

12501254
func testStream_ResultOnly_StreamResponse() async throws {
1251-
struct EmptyResponse: Decodable {}
1255+
struct EmptyResponse: Decodable, Sendable {}
12521256
let callable: Callable<EmptyRequest, StreamResponse<EmptyResponse, String>> = functions
12531257
.httpsCallable(
12541258
"genStreamResultOnly"

FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ public class FirebaseDataEncoder {
286286
/// - returns: A new `Data` value containing the encoded JSON data.
287287
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
288288
/// - throws: An error if any value throws an error during encoding.
289-
open func encode<T : Encodable>(_ value: T) throws -> Any {
289+
open func encode<T : Encodable>(_ value: T) throws -> sending Any {
290290
let encoder = __JSONEncoder(options: self.options)
291291

292292
guard let topLevel = try encoder.box_(value) else {

0 commit comments

Comments
 (0)