Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functions Codable after sharing implementation with RTDB #9091

Merged
merged 7 commits into from
Dec 16, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ jobs:
- name: Initialize xcodebuild
run: scripts/setup_spm_tests.sh
- name: iOS Unit Tests
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseFunctions iOS spmbuildonly
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FunctionsUnit iOS spm
- name: Integration Test Server
run: FirebaseFunctions/Backend/start.sh synchronous
- name: iOS Swift Integration Tests
- name: iOS Swift Integration Tests (Objective C library)
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FunctionsSwiftIntegration iOS spm
- name: iOS Swift Integration Tests (including Swift library)
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseFunctionsSwiftUnit iOS spm
- name: iOS Objective C Integration Tests
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FunctionsIntegration iOS spm
- name: Combine Unit Tests
Expand All @@ -68,7 +70,7 @@ jobs:
- name: Initialize xcodebuild
run: scripts/setup_spm_tests.sh
- name: Unit Tests
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseFunctions ${{ matrix.target }} spmbuildonly
run: scripts/third_party/travis/retry.sh ./scripts/build.sh FunctionsUnit ${{ matrix.target }} spm

catalyst:
# Don't run on private repo unless it is a PR.
Expand Down
23 changes: 23 additions & 0 deletions FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2021 Google LLC
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

import FirebaseDatabase
import FirebaseSharedSwift

extension Database {
public typealias Encoder = FirebaseDataEncoder
public typealias Decoder = FirebaseDataDecoder
}
102 changes: 102 additions & 0 deletions FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,105 @@ extension CurrencyAmount: ExpressibleByFloatLiteral {
self.value = Decimal(value)
}
}

paulb777 marked this conversation as resolved.
Show resolved Hide resolved
private func assertThat(_ dictionary: [String: Any],
file: StaticString = #file,
line: UInt = #line) -> DictionarySubject {
return DictionarySubject(dictionary, file: file, line: line)
}

func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file,
line: UInt = #line) -> CodableSubject<X> {
return CodableSubject(model, file: file, line: line)
}

func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file,
line: UInt = #line) -> EncodableSubject<X> {
return EncodableSubject(model, file: file, line: line)
}

class EncodableSubject<X: Equatable & Encodable> {
var subject: X
var file: StaticString
var line: UInt

init(_ subject: X, file: StaticString, line: UInt) {
self.subject = subject
self.file = file
self.line = line
}

@discardableResult
func encodes(to expected: [String: Any],
using encoder: Database.Encoder = .init()) -> DictionarySubject {
let encoded = assertEncodes(to: expected, using: encoder)
return DictionarySubject(encoded, file: file, line: line)
}

func failsToEncode() {
do {
let encoder = Database.Encoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
_ = try encoder.encode(subject)
} catch {
return
}
XCTFail("Failed to throw")
}

func failsEncodingAtTopLevel() {
do {
let encoder = Database.Encoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
_ = try encoder.encode(subject)
XCTFail("Failed to throw", file: file, line: line)
} catch EncodingError.invalidValue(_, _) {
return
} catch {
XCTFail("Unrecognized error: \(error)", file: file, line: line)
}
}

private func assertEncodes(to expected: [String: Any],
using encoder: Database.Encoder = .init()) -> [String: Any] {
do {
let enc = try encoder.encode(subject)
XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line)
return (enc as! NSDictionary) as! [String: Any]
} catch {
XCTFail("Failed to encode \(X.self): error: \(error)")
return ["": -1]
}
}
}

class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
func roundTrips(to expected: [String: Any],
using encoder: Database.Encoder = .init(),
decoder: Database.Decoder = .init()) {
let reverseSubject = encodes(to: expected, using: encoder)
reverseSubject.decodes(to: subject, using: decoder)
}
}

class DictionarySubject {
var subject: [String: Any]
var file: StaticString
var line: UInt

init(_ subject: [String: Any], file: StaticString, line: UInt) {
self.subject = subject
self.file = file
self.line = line
}

func decodes<X: Equatable & Codable>(to expected: X,
using decoder: Database.Decoder = .init()) -> Void {
do {
let decoded = try decoder.decode(X.self, from: subject)
XCTAssertEqual(decoded, expected)
} catch {
XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import FirebaseFunctions
import FirebaseFunctionsTestingSupport
import XCTest

/// This file was intitialized as a direct port of the Objective C
/// This file was initialized as a direct port of the Objective C
/// FirebaseFunctions/Tests/Integration/FIRIntegrationTests.m
///
/// The tests require the emulator to be running with `FirebaseFunctions/Backend/start.sh synchronous`
Expand Down
182 changes: 182 additions & 0 deletions FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2021 Google LLC
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.

import Foundation
import FirebaseFunctions
import FirebaseSharedSwift

public extension Functions {
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
/// request and the type of a `Decodable` response.
/// - Parameter name: The name of the Callable HTTPS trigger
/// - Parameter requestType: The type of the `Encodable` entity to use for requests to this `Callable`
/// - Parameter responseType: The type of the `Decodable` entity to use for responses from this `Callable`
func httpsCallable<Request: Encodable,
Response: Decodable>(_ name: String,
requestAs requestType: Request.Type = Request.self,
responseAs responseType: Response.Type = Response.self,
encoder: FirebaseDataEncoder = FirebaseDataEncoder(),
decoder: FirebaseDataDecoder = FirebaseDataDecoder())
-> Callable<Request, Response> {
return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder)
}
}

// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions.
public struct Callable<Request: Encodable, Response: Decodable> {
/// The timeout to use when calling the function. Defaults to 60 seconds.
public var timeoutInterval: TimeInterval {
get {
callable.timeoutInterval
}
set {
callable.timeoutInterval = newValue
}
}

enum CallableError: Error {
case internalError
}
paulb777 marked this conversation as resolved.
Show resolved Hide resolved

private let callable: HTTPSCallable
private let encoder: FirebaseDataEncoder
private let decoder: FirebaseDataDecoder

init(callable: HTTPSCallable, encoder: FirebaseDataEncoder, decoder: FirebaseDataDecoder) {
self.callable = callable
self.encoder = encoder
self.decoder = decoder
}

/// Executes this Callable HTTPS trigger asynchronously.
///
/// The data passed into the trigger must be of the generic `Request` type:
///
/// The request to the Cloud Functions backend made by this method automatically includes a
/// FCM token to identify the app instance. If a user is logged in with Firebase
/// Auth, an auth ID token for the user is also automatically included.
///
/// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect information
/// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
/// resumes with a new FCM Token the next time you call this method.
///
/// - Parameter data: Parameters to pass to the trigger.
/// - Parameter completion: The block to call when the HTTPS request has completed.
public func call(_ data: Request? = nil,
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
completion: @escaping (Result<Response, Error>)
-> Void) {
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
do {
let encoded = try encoder.encode(data)

callable.call(encoded) { result, error in
do {
if let result = result {
let decoded = try decoder.decode(Response.self, from: result.data)
completion(.success(decoded))
} else if let error = error {
completion(.failure(error))
} else {
completion(.failure(CallableError.internalError))
}
} catch {
completion(.failure(error))
}
}
} catch {
completion(.failure(error))
}
}

/// Creates a directly callable function.
///
/// This allows users to call a HTTPS Callable Function like a normal Swift function:
/// ```swift
/// let greeter = functions.httpsCallable("greeter",
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
/// requestType: GreetingRequest.self,
/// responseType: GreetingResponse.self)
/// greeter(data) { result in
/// print(result.greeting)
/// }
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
/// ```
/// You can also call a HTTPS Callable function using the following syntax:
/// ```swift
/// let greeter: Callable<GreetingRequest, GreetingResponse> = functions.httpsCallable("greeter")
/// greeter(data) { result in
/// print(result.greeting)
/// }
/// ```
/// - Parameters:
/// - data: Parameters to pass to the trigger.
/// - completion: The block to call when the HTTPS request has completed.
public func callAsFunction(_ data: Request? = nil,
completion: @escaping (Result<Response, Error>)
-> Void) {
call(data, completion: completion)
}

#if compiler(>=5.5) && canImport(_Concurrency)
/// Executes this Callable HTTPS trigger asynchronously.
///
/// The data passed into the trigger must be of the generic `Request` type:
///
/// The request to the Cloud Functions backend made by this method automatically includes a
/// FCM token to identify the app instance. If a user is logged in with Firebase
/// Auth, an auth ID token for the user is also automatically included.
///
/// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect information
/// regarding the app instance. To stop this, see `Messaging.deleteData()`. It
/// resumes with a new FCM Token the next time you call this method.
///
/// - Parameter data: The `Request` representing the data to pass to the trigger.
///
/// - Throws: An error if any value throws an error during encoding.
/// - Throws: An error if any value throws an error during decoding.
/// - Throws: An error if the callable fails to complete
///
/// - Returns: The decoded `Response` value
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
public func call(_ data: Request? = nil,
encoder: FirebaseDataEncoder = FirebaseDataEncoder(),
decoder: FirebaseDataDecoder =
FirebaseDataDecoder()) async throws -> Response {
let encoded = try encoder.encode(data)
let result = try await callable.call(encoded)
return try decoder.decode(Response.self, from: result.data)
}

/// Creates a directly callable function.
///
/// This allows users to call a HTTPS Callable Function like a normal Swift function:
/// ```swift
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
/// let greeter = functions.httpsCallable("greeter",
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
/// requestType: GreetingRequest.self,
/// responseType: GreetingResponse.self)
/// let result = try await greeter(data)
/// print(result.greeting)
/// ```
/// You can also call a HTTPS Callable function using the following syntax:
/// ```swift
/// let greeter: Callable<GreetingRequest, GreetingResponse> = functions.httpsCallable("greeter")
/// let result = try await greeter(data)
/// print(result.greeting)
/// ```
/// - Parameters:
/// - data: Parameters to pass to the trigger.
/// - Returns: The decoded `Response` value
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
public func callAsFunction(_ data: Request? = nil) async throws -> Response {
return try await call(data)
}
#endif
}