From e9252d29dd2fd295ae9486e58e192c0bd3359bef Mon Sep 17 00:00:00 2001 From: Morten Bek Ditlevsen Date: Sat, 23 Oct 2021 13:22:52 +0200 Subject: [PATCH 1/7] Make Shared Firebase Codable from RTDB Codable --- .../Sources/Codable/EncoderDecoder.swift | 23 + .../Codable/ServerValueCodingTests.swift | 111 ++++ FirebaseSharedSwift/CHANGELOG.md | 1 + .../third_party/StructureEncoder}/LICENSE | 0 .../third_party/StructureEncoder}/METADATA | 0 .../StructureEncoder/StructureEncoder.swift | 621 +++++++++--------- .../Tests/Codable/StructureEncoderTests.swift | 27 +- .../Tests/third_party/EncoderTests.swift | 115 ++-- Package.swift | 21 +- .../FirebaseSharedSwiftTests.xcscheme | 52 ++ 10 files changed, 580 insertions(+), 391 deletions(-) create mode 100644 FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift create mode 100644 FirebaseSharedSwift/CHANGELOG.md rename {FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder => FirebaseSharedSwift/Sources/third_party/StructureEncoder}/LICENSE (100%) rename {FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder => FirebaseSharedSwift/Sources/third_party/StructureEncoder}/METADATA (100%) rename FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/RTDBEncoder.swift => FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift (83%) rename FirebaseDatabaseSwift/Tests/Codable/RTDBEncoderTests.swift => FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift (95%) rename {FirebaseDatabaseSwift => FirebaseSharedSwift}/Tests/third_party/EncoderTests.swift (96%) create mode 100644 scripts/spm_test_schemes/FirebaseSharedSwiftTests.xcscheme diff --git a/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift b/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift new file mode 100644 index 00000000000..03bf696c4e1 --- /dev/null +++ b/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift @@ -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 = StructureEncoder + public typealias Decoder = StructureDecoder +} diff --git a/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift b/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift index 90567772ef0..259f1eee3e4 100644 --- a/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift +++ b/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift @@ -87,3 +87,114 @@ extension CurrencyAmount: ExpressibleByFloatLiteral { self.value = Decimal(value) } } + +private func assertThat(_ dictionary: [String: Any], + file: StaticString = #file, + line: UInt = #line) -> DictionarySubject { + return DictionarySubject(dictionary, file: file, line: line) +} + +func assertThat(_ model: X, file: StaticString = #file, + line: UInt = #line) -> CodableSubject { + return CodableSubject(model, file: file, line: line) +} + +func assertThat(_ model: X, file: StaticString = #file, + line: UInt = #line) -> EncodableSubject { + return EncodableSubject(model, file: file, line: line) +} + +class EncodableSubject { + 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: EncodableSubject { + 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(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) + } + } + + func failsDecoding(to _: X.Type, + using decoder: Database.Decoder = .init()) -> Void { + XCTAssertThrowsError( + try decoder.decode(X.self, from: subject), + file: file, + line: line + ) + } +} diff --git a/FirebaseSharedSwift/CHANGELOG.md b/FirebaseSharedSwift/CHANGELOG.md new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/FirebaseSharedSwift/CHANGELOG.md @@ -0,0 +1 @@ + diff --git a/FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/LICENSE b/FirebaseSharedSwift/Sources/third_party/StructureEncoder/LICENSE similarity index 100% rename from FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/LICENSE rename to FirebaseSharedSwift/Sources/third_party/StructureEncoder/LICENSE diff --git a/FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/METADATA b/FirebaseSharedSwift/Sources/third_party/StructureEncoder/METADATA similarity index 100% rename from FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/METADATA rename to FirebaseSharedSwift/Sources/third_party/StructureEncoder/METADATA diff --git a/FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/RTDBEncoder.swift b/FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift similarity index 83% rename from FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/RTDBEncoder.swift rename to FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift index c6533eb7dbf..c6aec8351b1 100644 --- a/FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/RTDBEncoder.swift +++ b/FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import FirebaseDatabase import Foundation extension DecodingError { @@ -96,188 +95,186 @@ extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, // used in the new runtime. _TtC10Foundation13__JSONEncoder is the // mangled name for Foundation.__JSONEncoder. -extension Database { - public class Encoder { - // MARK: Options - - /// The strategy to use for encoding `Date` values. - public enum DateEncodingStrategy { - /// Defer to `Date` for choosing an encoding. This is the default strategy. - case deferredToDate - - /// Encode the `Date` as a UNIX timestamp (as a JSON number). - case secondsSince1970 - - /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number). - case millisecondsSince1970 - - /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 - - /// Encode the `Date` as a string formatted by the given formatter. - case formatted(DateFormatter) - - /// Encode the `Date` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Date, Swift.Encoder) throws -> Void) - } - - /// The strategy to use for encoding `Data` values. - public enum DataEncodingStrategy { - /// Defer to `Data` for choosing an encoding. - case deferredToData - - /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. - case base64 - - /// Encode the `Data` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Data, Swift.Encoder) throws -> Void) - } - - /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). - public enum NonConformingFloatEncodingStrategy { - /// Throw upon encountering non-conforming values. This is the default strategy. - case `throw` - - /// Encode the values using the given representation strings. - case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) - } - - /// The strategy to use for automatically changing the value of keys before encoding. - public enum KeyEncodingStrategy { - /// Use the keys specified by each type. This is the default strategy. - case useDefaultKeys - - /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload. - /// - /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt). - /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. - /// - /// Converting from camel case to snake case: - /// 1. Splits words at the boundary of lower-case to upper-case - /// 2. Inserts `_` between words - /// 3. Lowercases the entire string - /// 4. Preserves starting and ending `_`. - /// - /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. - /// - /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. - case convertToSnakeCase - - /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types. - /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding. - /// If the result of the conversion is a duplicate key, then only one value will be present in the result. - case custom((_ codingPath: [CodingKey]) -> CodingKey) - - fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String { - guard !stringKey.isEmpty else { return stringKey } - - var words : [Range] = [] - // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase - // - // myProperty -> my_property - // myURLProperty -> my_url_property - // - // We assume, per Swift naming conventions, that the first character of the key is lowercase. - var wordStart = stringKey.startIndex - var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. - let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) - words.append(upperCaseRange.lowerBound.. Void) + } + + /// The strategy to use for encoding `Data` values. + public enum DataEncodingStrategy { + /// Defer to `Data` for choosing an encoding. + case deferredToData + + /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. + case base64 + + /// Encode the `Data` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Data, Swift.Encoder) throws -> Void) + } + + /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). + public enum NonConformingFloatEncodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + case `throw` + + /// Encode the values using the given representation strings. + case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) + } + + /// The strategy to use for automatically changing the value of keys before encoding. + public enum KeyEncodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys + + /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload. + /// + /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt). + /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. + /// + /// Converting from camel case to snake case: + /// 1. Splits words at the boundary of lower-case to upper-case + /// 2. Inserts `_` between words + /// 3. Lowercases the entire string + /// 4. Preserves starting and ending `_`. + /// + /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. + /// + /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. + case convertToSnakeCase + + /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types. + /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding. + /// If the result of the conversion is a duplicate key, then only one value will be present in the result. + case custom((_ codingPath: [CodingKey]) -> CodingKey) + + fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + + var words : [Range] = [] + // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase + // + // myProperty -> my_property + // myURLProperty -> my_url_property + // + // We assume, per Swift naming conventions, that the first character of the key is lowercase. + var wordStart = stringKey.startIndex + var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. + let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) + words.append(upperCaseRange.lowerBound..(_ value: T) throws -> Any { - let encoder = __JSONEncoder(options: self.options) - - guard let topLevel = try encoder.box_(value) else { - throw Swift.EncodingError.invalidValue(value, - Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values.")) - } - return topLevel + /// Encodes the given top-level value and returns its JSON representation. + /// + /// - parameter value: The value to encode. + /// - returns: A new `Data` value containing the encoded JSON data. + /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`. + /// - throws: An error if any value throws an error during encoding. + open func encode(_ value: T) throws -> Any { + let encoder = __JSONEncoder(options: self.options) + + guard let topLevel = try encoder.box_(value) else { + throw Swift.EncodingError.invalidValue(value, + Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values.")) } + return topLevel } } @@ -293,7 +290,7 @@ fileprivate class __JSONEncoder : Encoder { fileprivate var storage: _JSONEncodingStorage /// Options set on the top-level encoder. - fileprivate let options: Database.Encoder._Options + fileprivate let options: StructureEncoder._Options /// The path to the current point in encoding. public var codingPath: [CodingKey] @@ -306,7 +303,7 @@ fileprivate class __JSONEncoder : Encoder { // MARK: - Initialization /// Initializes `self` with the given top-level encoder options. - fileprivate init(options: Database.Encoder._Options, codingPath: [CodingKey] = []) { + fileprivate init(options: StructureEncoder._Options, codingPath: [CodingKey] = []) { self.options = options self.storage = _JSONEncodingStorage() self.codingPath = codingPath @@ -440,7 +437,7 @@ fileprivate struct _JSONKeyedEncodingContainer : KeyedEncodingCon case .useDefaultKeys: return key case .convertToSnakeCase: - let newKeyString = Database.Encoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue) + let newKeyString = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue) return _JSONKey(stringValue: newKeyString, intValue: key.intValue) case .custom(let converter): return converter(codingPath + [key]) @@ -1042,177 +1039,175 @@ fileprivate class __JSONReferencingEncoder : __JSONEncoder { // The two must coexist, so it was renamed. The old name must not be // used in the new runtime. _TtC10Foundation13__JSONDecoder is the // mangled name for Foundation.__JSONDecoder. -extension Database { - public class Decoder { - // MARK: Options +public class StructureDecoder { + // MARK: Options - /// The strategy to use for decoding `Date` values. - public enum DateDecodingStrategy { - /// Defer to `Date` for decoding. This is the default strategy. - case deferredToDate + /// The strategy to use for decoding `Date` values. + public enum DateDecodingStrategy { + /// Defer to `Date` for decoding. This is the default strategy. + case deferredToDate - /// Decode the `Date` as a UNIX timestamp from a JSON number. - case secondsSince1970 + /// Decode the `Date` as a UNIX timestamp from a JSON number. + case secondsSince1970 - /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. - case millisecondsSince1970 + /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. + case millisecondsSince1970 - /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 + /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 - /// Decode the `Date` as a string parsed by the given formatter. - case formatted(DateFormatter) + /// Decode the `Date` as a string parsed by the given formatter. + case formatted(DateFormatter) - /// Decode the `Date` as a custom value decoded by the given closure. - case custom((_ decoder: Swift.Decoder) throws -> Date) - } + /// Decode the `Date` as a custom value decoded by the given closure. + case custom((_ decoder: Swift.Decoder) throws -> Date) + } - /// The strategy to use for decoding `Data` values. - public enum DataDecodingStrategy { - /// Defer to `Data` for decoding. - case deferredToData + /// The strategy to use for decoding `Data` values. + public enum DataDecodingStrategy { + /// Defer to `Data` for decoding. + case deferredToData - /// Decode the `Data` from a Base64-encoded string. This is the default strategy. - case base64 + /// Decode the `Data` from a Base64-encoded string. This is the default strategy. + case base64 - /// Decode the `Data` as a custom value decoded by the given closure. - case custom((_ decoder: Swift.Decoder) throws -> Data) - } + /// Decode the `Data` as a custom value decoded by the given closure. + case custom((_ decoder: Swift.Decoder) throws -> Data) + } - /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). - public enum NonConformingFloatDecodingStrategy { - /// Throw upon encountering non-conforming values. This is the default strategy. - case `throw` + /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). + public enum NonConformingFloatDecodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + case `throw` - /// Decode the values from the given representation strings. - case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) - } + /// Decode the values from the given representation strings. + case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) + } - /// The strategy to use for automatically changing the value of keys before decoding. - public enum KeyDecodingStrategy { - /// Use the keys specified by each type. This is the default strategy. - case useDefaultKeys + /// The strategy to use for automatically changing the value of keys before decoding. + public enum KeyDecodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys - /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type. - /// - /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. - /// - /// Converting from snake case to camel case: - /// 1. Capitalizes the word starting after each `_` - /// 2. Removes all `_` - /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata). - /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`. - /// - /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character. - case convertFromSnakeCase + /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type. + /// + /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. + /// + /// Converting from snake case to camel case: + /// 1. Capitalizes the word starting after each `_` + /// 2. Removes all `_` + /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata). + /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`. + /// + /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character. + case convertFromSnakeCase - /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types. - /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding. - /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from. - case custom((_ codingPath: [CodingKey]) -> CodingKey) + /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types. + /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding. + /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from. + case custom((_ codingPath: [CodingKey]) -> CodingKey) - fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String { - guard !stringKey.isEmpty else { return stringKey } + fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } - // Find the first non-underscore character - guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else { - // Reached the end without finding an _ - return stringKey - } + // Find the first non-underscore character + guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else { + // Reached the end without finding an _ + return stringKey + } - // Find the last non-underscore character - var lastNonUnderscore = stringKey.index(before: stringKey.endIndex) - while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" { - stringKey.formIndex(before: &lastNonUnderscore) - } + // Find the last non-underscore character + var lastNonUnderscore = stringKey.index(before: stringKey.endIndex) + while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" { + stringKey.formIndex(before: &lastNonUnderscore) + } - let keyRange = firstNonUnderscore...lastNonUnderscore - let leadingUnderscoreRange = stringKey.startIndex..(_ type: T.Type, from structure: Any) throws -> T { - let decoder = __JSONDecoder(referencing: structure, options: self.options) - guard let value = try decoder.unbox(structure, as: type) else { - throw Swift.DecodingError.valueNotFound(type, Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) - } + // MARK: - Decoding Values - return value + /// Decodes a top-level value of the given type from the given JSON representation. + /// + /// - parameter type: The type of the value to decode. + /// - parameter data: The data to decode from. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON. + /// - throws: An error if any value throws an error during decoding. + open func decode(_ type: T.Type, from structure: Any) throws -> T { + let decoder = __JSONDecoder(referencing: structure, options: self.options) + guard let value = try decoder.unbox(structure, as: type) else { + throw Swift.DecodingError.valueNotFound(type, Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) } + + return value } } @@ -1228,7 +1223,7 @@ fileprivate class __JSONDecoder : Decoder { fileprivate var storage: _JSONDecodingStorage /// Options set on the top-level decoder. - fileprivate let options: Database.Decoder._Options + fileprivate let options: StructureDecoder._Options /// The path to the current point in encoding. fileprivate(set) public var codingPath: [CodingKey] @@ -1241,7 +1236,7 @@ fileprivate class __JSONDecoder : Decoder { // MARK: - Initialization /// Initializes `self` with the given top-level container and options. - fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: Database.Decoder._Options) { + fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: StructureDecoder._Options) { self.storage = _JSONDecodingStorage() self.storage.push(container: container) self.codingPath = codingPath @@ -1347,7 +1342,7 @@ fileprivate struct _JSONKeyedDecodingContainer : KeyedDecodingCon // Convert the snake case keys in the container to camel case. // If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with JSON dictionaries. self.container = Dictionary(container.map { - key, value in (Database.Decoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) + key, value in (StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) }, uniquingKeysWith: { (first, _) in first }) case .custom(let converter): self.container = Dictionary(container.map { @@ -1372,8 +1367,8 @@ fileprivate struct _JSONKeyedDecodingContainer : KeyedDecodingCon case .convertFromSnakeCase: // In this case we can attempt to recover the original value by reversing the transform let original = key.stringValue - let converted = Database.Encoder.KeyEncodingStrategy._convertToSnakeCase(original) - let roundtrip = Database.Decoder.KeyDecodingStrategy._convertFromSnakeCase(converted) + let converted = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(original) + let roundtrip = StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(converted) if converted == original { return "\(key) (\"\(original)\")" } else if roundtrip == original { diff --git a/FirebaseDatabaseSwift/Tests/Codable/RTDBEncoderTests.swift b/FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift similarity index 95% rename from FirebaseDatabaseSwift/Tests/Codable/RTDBEncoderTests.swift rename to FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift index 50000146b73..cecf1d1c1d0 100644 --- a/FirebaseDatabaseSwift/Tests/Codable/RTDBEncoderTests.swift +++ b/FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift @@ -15,11 +15,10 @@ */ import Foundation -import FirebaseDatabase -import FirebaseDatabaseSwift +import FirebaseSharedSwift import XCTest -class FirebaseDatabaseEncoderTests: XCTestCase { +class FirebaseStructureEncoderTests: XCTestCase { func testInt() { struct Model: Codable, Equatable { let x: Int @@ -30,7 +29,7 @@ class FirebaseDatabaseEncoderTests: XCTestCase { } func testNullDecodesAsNil() throws { - let decoder = Database.Decoder() + let decoder = StructureDecoder() let opt = try decoder.decode(Int?.self, from: NSNull()) XCTAssertNil(opt) } @@ -53,9 +52,9 @@ class FirebaseDatabaseEncoderTests: XCTestCase { } let model = Model(snakeCase: 42) let dict = ["snake_case": 42] - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase assertThat(model).roundTrips(to: dict, using: encoder, decoder: decoder) } @@ -419,14 +418,14 @@ class EncodableSubject { @discardableResult func encodes(to expected: [String: Any], - using encoder: Database.Encoder = .init()) -> DictionarySubject { + using encoder: StructureEncoder = .init()) -> DictionarySubject { let encoded = assertEncodes(to: expected, using: encoder) return DictionarySubject(encoded, file: file, line: line) } func failsToEncode() { do { - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase _ = try encoder.encode(subject) } catch { @@ -437,7 +436,7 @@ class EncodableSubject { func failsEncodingAtTopLevel() { do { - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase _ = try encoder.encode(subject) XCTFail("Failed to throw", file: file, line: line) @@ -449,7 +448,7 @@ class EncodableSubject { } private func assertEncodes(to expected: [String: Any], - using encoder: Database.Encoder = .init()) -> [String: Any] { + using encoder: StructureEncoder = .init()) -> [String: Any] { do { let enc = try encoder.encode(subject) XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line) @@ -463,8 +462,8 @@ class EncodableSubject { class CodableSubject: EncodableSubject { func roundTrips(to expected: [String: Any], - using encoder: Database.Encoder = .init(), - decoder: Database.Decoder = .init()) { + using encoder: StructureEncoder = .init(), + decoder: StructureDecoder = .init()) { let reverseSubject = encodes(to: expected, using: encoder) reverseSubject.decodes(to: subject, using: decoder) } @@ -482,7 +481,7 @@ class DictionarySubject { } func decodes(to expected: X, - using decoder: Database.Decoder = .init()) -> Void { + using decoder: StructureDecoder = .init()) -> Void { do { let decoded = try decoder.decode(X.self, from: subject) XCTAssertEqual(decoded, expected) @@ -492,7 +491,7 @@ class DictionarySubject { } func failsDecoding(to _: X.Type, - using decoder: Database.Decoder = .init()) -> Void { + using decoder: StructureDecoder = .init()) -> Void { XCTAssertThrowsError( try decoder.decode(X.self, from: subject), file: file, diff --git a/FirebaseDatabaseSwift/Tests/third_party/EncoderTests.swift b/FirebaseSharedSwift/Tests/third_party/EncoderTests.swift similarity index 96% rename from FirebaseDatabaseSwift/Tests/third_party/EncoderTests.swift rename to FirebaseSharedSwift/Tests/third_party/EncoderTests.swift index 71960fed4ee..e088baab3d3 100644 --- a/FirebaseDatabaseSwift/Tests/third_party/EncoderTests.swift +++ b/FirebaseSharedSwift/Tests/third_party/EncoderTests.swift @@ -8,8 +8,7 @@ // //===----------------------------------------------------------------------===// -import FirebaseDatabase -import FirebaseDatabaseSwift +import FirebaseSharedSwift import Swift import Foundation @@ -17,7 +16,7 @@ import Foundation import XCTest -class TestDatabaseEncoder: XCTestCase { +class TestStructureEncoder: XCTestCase { // MARK: - Encoding Top-Level Empty Types func testEncodingTopLevelEmptyStruct() { @@ -216,14 +215,14 @@ class TestDatabaseEncoder: XCTestCase { func localTestRoundTrip(of value: T) { var payload: Any! = nil do { - let encoder = Database.Encoder() + let encoder = StructureEncoder() payload = try encoder.encode(value) } catch { XCTFail("Failed to encode \(T.self): \(error)") } do { - let decoder = Database.Decoder() + let decoder = StructureDecoder() let decoded = try decoder.decode(T.self, from: payload!) /// `snprintf`'s `%g`, which `JSONSerialization` uses internally for double values, does not respect @@ -475,12 +474,12 @@ class TestDatabaseEncoder: XCTestCase { } func testEncodingNonConformingFloatStrings() { - let encodingStrategy: Database.Encoder.NonConformingFloatEncodingStrategy = .convertToString( + let encodingStrategy: StructureEncoder.NonConformingFloatEncodingStrategy = .convertToString( positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN" ) - let decodingStrategy: Database.Decoder.NonConformingFloatDecodingStrategy = .convertFromString( + let decodingStrategy: StructureDecoder.NonConformingFloatDecodingStrategy = .convertFromString( positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN" @@ -582,7 +581,7 @@ class TestDatabaseEncoder: XCTestCase { let expected = ["\(test.1)": "test"] let encoded = EncodeMe(keyName: test.0) - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(encoded) @@ -594,7 +593,7 @@ class TestDatabaseEncoder: XCTestCase { let expected = ["QQQhello": "test"] let encoded = EncodeMe(keyName: "hello") - let encoder = Database.Encoder() + let encoder = StructureEncoder() let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)! return key @@ -608,7 +607,7 @@ class TestDatabaseEncoder: XCTestCase { func testEncodingDictionaryStringKeyConversionUntouched() { let toEncode = ["leaveMeAlone": "test"] - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(toEncode) @@ -626,7 +625,7 @@ class TestDatabaseEncoder: XCTestCase { func testEncodingDictionaryFailureKeyPath() { let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)] - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -643,7 +642,7 @@ class TestDatabaseEncoder: XCTestCase { let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]] - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -673,7 +672,7 @@ class TestDatabaseEncoder: XCTestCase { let encoded = EncodeNestedNested(outerValue: EncodeNested(nestedValue: EncodeMe(keyName: "helloWorld"))) - let encoder = Database.Encoder() + let encoder = StructureEncoder() var callCount = 0 let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in @@ -758,7 +757,7 @@ class TestDatabaseEncoder: XCTestCase { // This structure contains the camel case key that the test object should decode with, then it uses the snake case key (test.0) as the actual key for the boolean value. let input = ["camelCaseKey": "\(test.1)", "\(test.0)": true] as [String: Any] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode(DecodeMe.self, from: input) @@ -771,7 +770,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecodingKeyStrategyCustom() { let input = ["----hello": "test"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in // This converter removes the first 4 characters from the start of all string keys, if it has more than 4 characters let string = path.last!.stringValue @@ -787,7 +786,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecodingDictionaryStringKeyConversionUntouched() { let input = ["leave_me_alone": "test"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode([String: String].self, from: input) @@ -796,7 +795,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecodingDictionaryFailureKeyPath() { let input = ["leave_me_alone": "test"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { _ = try decoder.decode([String: Int].self, from: input) @@ -818,7 +817,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecodingDictionaryFailureKeyPathNested() { let input = ["top_level": ["sub_level": ["nested_value": ["int_value": "not_an_int"]]]] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { _ = try decoder.decode([String: [String: DecodeFailureNested]].self, from: input) @@ -840,7 +839,7 @@ class TestDatabaseEncoder: XCTestCase { func testEncodingKeyStrategySnakeGenerated() { // Test that this works with a struct that has automatically generated keys let input = ["this_is_camel_case": "test"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode(DecodeMe3.self, from: input) @@ -849,7 +848,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecodingKeyStrategyCamelGenerated() { let encoded = DecodeMe3(thisIsCamelCase: "test") - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(encoded) XCTAssertEqual(["this_is_camel_case": "test"], result as? [String: String]) @@ -868,7 +867,7 @@ class TestDatabaseEncoder: XCTestCase { // Decoding let input = ["foo_bar": "test", "this_is_camel_case_too": "test2"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let decodingResult = try! decoder.decode(DecodeMe4.self, from: input) @@ -877,7 +876,7 @@ class TestDatabaseEncoder: XCTestCase { // Encoding let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2") - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let encodingResult = try! encoder.encode(encoded) XCTAssertEqual( @@ -923,7 +922,7 @@ class TestDatabaseEncoder: XCTestCase { // Decoding // This input has a dictionary with two keys, but only one will end up in the container let input = ["unused key 1": "test1", "unused key 2": "test2"] - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.keyDecodingStrategy = .custom(customKeyConversion) let decodingResult = try! decoder.decode(DecodeMe5.self, from: input) @@ -932,7 +931,7 @@ class TestDatabaseEncoder: XCTestCase { // Encoding let encoded = DecodeMe5() - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.keyEncodingStrategy = .custom(customKeyConversion) let decodingResult2 = try! encoder.encode(encoded) @@ -943,7 +942,7 @@ class TestDatabaseEncoder: XCTestCase { // MARK: - Encoder Features func testNestedContainerCodingPaths() { - let encoder = Database.Encoder() + let encoder = StructureEncoder() do { _ = try encoder.encode(NestedContainersTestType()) } catch let error as NSError { @@ -952,7 +951,7 @@ class TestDatabaseEncoder: XCTestCase { } func testSuperEncoderCodingPaths() { - let encoder = Database.Encoder() + let encoder = StructureEncoder() do { _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } catch let error as NSError { @@ -976,7 +975,7 @@ class TestDatabaseEncoder: XCTestCase { } func testInterceptURL() { - // Want to make sure Database.Encoder writes out single-value URLs, not the keyed encoding. + // Want to make sure StructureEncoder writes out single-value URLs, not the keyed encoding. let expected = "http://swift.org" let url = URL(string: "http://swift.org")! _testRoundTrip(of: url, expected: expected) @@ -1015,13 +1014,13 @@ class TestDatabaseEncoder: XCTestCase { } func testDecodingConcreteTypeParameter() { - let encoder = Database.Encoder() + let encoder = StructureEncoder() guard let value = try? encoder.encode(Employee.testValue) else { XCTFail("Unable to encode Employee.") return } - let decoder = Database.Decoder() + let decoder = StructureDecoder() guard let decoded = try? decoder.decode(Employee.self as Person.Type, from: value) else { XCTFail("Failed to decode Employee as Person.") return @@ -1062,7 +1061,7 @@ class TestDatabaseEncoder: XCTestCase { // // The issue at hand reproduces when you have a referencing encoder (superEncoder() creates one) that has a container on the stack (unkeyedContainer() adds one) that encodes a value going through box_() (Array does that) that encodes something which throws (Float.infinity does that). // When reproducing, this will cause a test failure via fatalError(). - _ = try? Database.Encoder().encode(ReferencingEncoderWrapper([Float.infinity])) + _ = try? StructureEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) } func testEncoderStateThrowOnEncodeCustomDate() { @@ -1079,7 +1078,7 @@ class TestDatabaseEncoder: XCTestCase { } // The closure needs to push a container before throwing an error to trigger. - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.dateEncodingStrategy = .custom { _, encoder in _ = encoder.unkeyedContainer() enum CustomError: Error { case foo } @@ -1103,7 +1102,7 @@ class TestDatabaseEncoder: XCTestCase { } // The closure needs to push a container before throwing an error to trigger. - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.dataEncodingStrategy = .custom { _, encoder in _ = encoder.unkeyedContainer() enum CustomError: Error { case foo } @@ -1121,12 +1120,12 @@ class TestDatabaseEncoder: XCTestCase { // Once Array decoding begins, 1 is pushed onto the container stack ([[1,2,3], 1]), and 1 is attempted to be decoded as String. This throws a .typeMismatch, but the container is not popped off the stack. // When attempting to decode [Int], the container stack is still ([[1,2,3], 1]), and 1 fails to decode as [Int]. let input = [1, 2, 3] - _ = try! Database.Decoder().decode(EitherDecodable<[String], [Int]>.self, from: input) + _ = try! StructureDecoder().decode(EitherDecodable<[String], [Int]>.self, from: input) } func testDecoderStateThrowOnDecodeCustomDate() { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.dateDecodingStrategy = .custom { decoder in enum CustomError: Error { case foo } throw CustomError.foo @@ -1138,7 +1137,7 @@ class TestDatabaseEncoder: XCTestCase { func testDecoderStateThrowOnDecodeCustomData() { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.dataDecodingStrategy = .custom { decoder in enum CustomError: Error { case foo } throw CustomError.foo @@ -1156,34 +1155,34 @@ class TestDatabaseEncoder: XCTestCase { private func _testEncodeFailure(of value: T) { do { - _ = try Database.Encoder().encode(value) + _ = try StructureEncoder().encode(value) XCTFail("Encode of top-level \(T.self) was expected to fail.") } catch {} } private func _testRoundTrip(of value: T, expected: U, - dateEncodingStrategy: Database.Encoder + dateEncodingStrategy: StructureEncoder .DateEncodingStrategy = .deferredToDate, - dateDecodingStrategy: Database.Decoder + dateDecodingStrategy: StructureDecoder .DateDecodingStrategy = .deferredToDate, - dataEncodingStrategy: Database.Encoder + dataEncodingStrategy: StructureEncoder .DataEncodingStrategy = .base64, - dataDecodingStrategy: Database.Decoder + dataDecodingStrategy: StructureDecoder .DataDecodingStrategy = .base64, - keyEncodingStrategy: Database.Encoder + keyEncodingStrategy: StructureEncoder .KeyEncodingStrategy = .useDefaultKeys, - keyDecodingStrategy: Database.Decoder + keyDecodingStrategy: StructureDecoder .KeyDecodingStrategy = .useDefaultKeys, - nonConformingFloatEncodingStrategy: Database.Encoder + nonConformingFloatEncodingStrategy: StructureEncoder .NonConformingFloatEncodingStrategy = .throw, - nonConformingFloatDecodingStrategy: Database.Decoder + nonConformingFloatDecodingStrategy: StructureDecoder .NonConformingFloatDecodingStrategy = .throw) where T: Codable, T: Equatable, U: Equatable { var payload: Any! = nil do { - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy @@ -1200,7 +1199,7 @@ class TestDatabaseEncoder: XCTestCase { ) do { - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy decoder.dataDecodingStrategy = dataDecodingStrategy decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy @@ -1213,26 +1212,26 @@ class TestDatabaseEncoder: XCTestCase { } private func _testRoundTrip(of value: T, - dateEncodingStrategy: Database.Encoder + dateEncodingStrategy: StructureEncoder .DateEncodingStrategy = .deferredToDate, - dateDecodingStrategy: Database.Decoder + dateDecodingStrategy: StructureDecoder .DateDecodingStrategy = .deferredToDate, - dataEncodingStrategy: Database.Encoder + dataEncodingStrategy: StructureEncoder .DataEncodingStrategy = .base64, - dataDecodingStrategy: Database.Decoder + dataDecodingStrategy: StructureDecoder .DataDecodingStrategy = .base64, - keyEncodingStrategy: Database.Encoder + keyEncodingStrategy: StructureEncoder .KeyEncodingStrategy = .useDefaultKeys, - keyDecodingStrategy: Database.Decoder + keyDecodingStrategy: StructureDecoder .KeyDecodingStrategy = .useDefaultKeys, - nonConformingFloatEncodingStrategy: Database.Encoder + nonConformingFloatEncodingStrategy: StructureEncoder .NonConformingFloatEncodingStrategy = .throw, - nonConformingFloatDecodingStrategy: Database.Decoder + nonConformingFloatDecodingStrategy: StructureDecoder .NonConformingFloatDecodingStrategy = .throw) where T: Codable, T: Equatable { var payload: Any! = nil do { - let encoder = Database.Encoder() + let encoder = StructureEncoder() encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy @@ -1243,7 +1242,7 @@ class TestDatabaseEncoder: XCTestCase { } do { - let decoder = Database.Decoder() + let decoder = StructureDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy decoder.dataDecodingStrategy = dataDecodingStrategy decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy @@ -1258,8 +1257,8 @@ class TestDatabaseEncoder: XCTestCase { private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T: Codable, U: Codable { do { - let data = try Database.Encoder().encode(value) - _ = try Database.Decoder().decode(U.self, from: data) + let data = try StructureEncoder().encode(value) + _ = try StructureDecoder().decode(U.self, from: data) XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.") } catch {} } diff --git a/Package.swift b/Package.swift index c5c34012dc8..f58be9fb10a 100644 --- a/Package.swift +++ b/Package.swift @@ -516,18 +516,27 @@ let package = Package( ), .target( name: "FirebaseDatabaseSwift", - dependencies: ["FirebaseDatabase"], - path: "FirebaseDatabaseSwift/Sources", - exclude: [ - "third_party/RTDBEncoder/LICENSE", - "third_party/RTDBEncoder/METADATA", - ] + dependencies: ["FirebaseDatabase", "FirebaseSharedSwift"], + path: "FirebaseDatabaseSwift/Sources" ), .testTarget( name: "FirebaseDatabaseSwiftTests", dependencies: ["FirebaseDatabase", "FirebaseDatabaseSwift"], path: "FirebaseDatabaseSwift/Tests/" ), + .target( + name: "FirebaseSharedSwift", + path: "FirebaseSharedSwift/Sources", + exclude: [ + "third_party/StructureEncoder/LICENSE", + "third_party/StructureEncoder/METADATA", + ] + ), + .testTarget( + name: "FirebaseSharedSwiftTests", + dependencies: ["FirebaseSharedSwift"], + path: "FirebaseSharedSwift/Tests/" + ), .target( name: "FirebaseDynamicLinksTarget", dependencies: [.target(name: "FirebaseDynamicLinks", diff --git a/scripts/spm_test_schemes/FirebaseSharedSwiftTests.xcscheme b/scripts/spm_test_schemes/FirebaseSharedSwiftTests.xcscheme new file mode 100644 index 00000000000..631477559f5 --- /dev/null +++ b/scripts/spm_test_schemes/FirebaseSharedSwiftTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + From aea5e89e9e19eb71a4ac429756ed5ce169f9ac36 Mon Sep 17 00:00:00 2001 From: Morten Bek Ditlevsen Date: Mon, 13 Dec 2021 20:59:58 +0100 Subject: [PATCH 2/7] [WIP] FirebaseFunctionsSwift (#8854) --- .../Sources/Codable/Callable+Codable.swift | 142 +++++ .../Tests/IntegrationTests.swift | 534 ++++++++++++++++++ Package.swift | 19 + 3 files changed, 695 insertions(+) create mode 100644 FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift create mode 100644 FirebaseFunctionsSwift/Tests/IntegrationTests.swift diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift new file mode 100644 index 00000000000..e6e1355d812 --- /dev/null +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -0,0 +1,142 @@ +/* + * Copyright 2020 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(_ name: String, + requestType: Request.Type, + responseType: Response.Type, + encoder: StructureEncoder = StructureEncoder(), + decoder: StructureDecoder = StructureDecoder()) + -> Callable { + 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 { + /** + * 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 + } + + private let callable: HTTPSCallable + private let encoder: StructureEncoder + private let decoder: StructureDecoder + + init(callable: HTTPSCallable, encoder: StructureEncoder, decoder: StructureDecoder) { + 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 + * Firebase Instance ID 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 Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see `[FIRInstanceID deleteIDWithHandler:]`. It + * resumes with a new Instance ID 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. + * + * - Throws: An error if any value throws an error during encoding. + */ + public func call(_ data: Request, + completion: @escaping (Result) + -> Void) throws { + 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)) + } + } + } + + #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 + * Firebase Instance ID 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 Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see `[FIRInstanceID deleteIDWithHandler:]`. It + * resumes with a new Instance ID 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, + encoder: StructureEncoder = StructureEncoder(), + decoder: StructureDecoder = + StructureDecoder()) 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) + } + #endif +} diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift new file mode 100644 index 00000000000..d9880f57ef6 --- /dev/null +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -0,0 +1,534 @@ +// 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 FirebaseFunctionsSwift +import FirebaseFunctionsTestingSupport +import XCTest + +/// This file was intitialized 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` +/// The Firebase Functions called in the tests are implemented in `FirebaseFunctions/Backend/index.js`. + +struct DataTestRequest: Encodable { + var bool: Bool + var int: Int32 + var long: Int64 + var string: String + var array: [Int32] + // NOTE: Auto-synthesized Encodable conformance uses 'encodeIfPresent' to + // encode Optional values. To encode Optional.none as null you either need + // to write a manual encodable conformance or use a helper like the + // propertyWrapper here: + @NullEncodable var null: Bool? +} + +@propertyWrapper +struct NullEncodable: Encodable where T: Encodable { + var wrappedValue: T? + + init(wrappedValue: T?) { + self.wrappedValue = wrappedValue + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch wrappedValue { + case let .some(value): try container.encode(value) + case .none: try container.encodeNil() + } + } +} + +struct DataTestResponse: Decodable, Equatable { + var message: String + var long: Int64 + var code: Int32 +} + +class IntegrationTests: XCTestCase { + let functions = FunctionsFake( + projectID: "functions-integration-test", + region: "us-central1", + customDomain: nil, + withToken: nil + ) + let projectID = "functions-swift-integration-test" + + override func setUp() { + super.setUp() + functions.useLocalhost() + } + + func testData() throws { + let expectation = expectation(description: #function) + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + let function = functions.httpsCallable("dataTest", + requestType: DataTestRequest.self, + responseType: DataTestResponse.self) + try function.call(data) { result in + do { + let response = try result.get() + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testDataAsync() async throws { + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + + let function = functions.httpsCallable("dataTest", + requestType: DataTestRequest.self, + responseType: DataTestResponse.self) + + let response = try await function.call(data) + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + } + #endif + + func testScalar() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "scalarTest", + requestType: Int16.self, + responseType: Int.self + ) + try function.call(17) { result in + do { + let response = try result.get() + XCTAssertEqual(response, 76) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testScalarAsync() async throws { + let function = functions.httpsCallable( + "scalarTest", + requestType: Int16.self, + responseType: Int.self + ) + + let result = try await function.call(17) + XCTAssertEqual(result, 76) + } + #endif + + func testToken() throws { + // Recreate functions with a token. + let functions = FunctionsFake( + projectID: "functions-integration-test", + region: "us-central1", + customDomain: nil, + withToken: "token" + ) + functions.useLocalhost() + + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "FCMTokenTest", + requestType: [String: Int].self, + responseType: [String: Int].self + ) + XCTAssertNotNil(function) + try function.call([:]) { result in + do { + let data = try result.get() + XCTAssertEqual(data, [:]) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testTokenAsync() async throws { + // Recreate functions with a token. + let functions = FunctionsFake( + projectID: "functions-integration-test", + region: "us-central1", + customDomain: nil, + withToken: "token" + ) + functions.useLocalhost() + + let function = functions.httpsCallable( + "FCMTokenTest", + requestType: [String: Int].self, + responseType: [String: Int].self + ) + + let data = try await function.call([:]) + XCTAssertEqual(data, [:]) + } + #endif + + func testFCMToken() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "FCMTokenTest", + requestType: [String: Int].self, + responseType: [String: Int].self + ) + try function.call([:]) { result in + do { + let data = try result.get() + XCTAssertEqual(data, [:]) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testFCMTokenAsync() async throws { + let function = functions.httpsCallable( + "FCMTokenTest", + requestType: [String: Int].self, + responseType: [String: Int].self + ) + + let data = try await function.call([:]) + XCTAssertEqual(data, [:]) + } + #endif + + func testNull() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "nullTest", + requestType: Int?.self, + responseType: Int?.self + ) + try function.call(nil) { result in + do { + let data = try result.get() + XCTAssertEqual(data, nil) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testNullAsync() async throws { + let function = functions.httpsCallable( + "nullTest", + requestType: Int?.self, + responseType: Int?.self + ) + + let data = try await function.call(nil) + XCTAssertEqual(data, nil) + } + #endif + + // No parameters to call should be the same as passing nil. + // If no parameters are required, then the non-typed API + // is more appropriate since it specifically avoids defining + // type. + // func testParameterless() { + // } + // + // + func testMissingResult() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "missingResultTest", + requestType: Int?.self, + responseType: Int?.self + ) + try function.call(nil) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("Response is missing data field.", error.localizedDescription) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testMissingResultAsync() async { + let function = functions.httpsCallable( + "missingResultTest", + requestType: Int?.self, + responseType: Int?.self + ) + do { + _ = try await function.call(nil) + XCTFail("Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("Response is missing data field.", error.localizedDescription) + } + } + #endif + + func testUnhandledError() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "unhandledErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + try function.call([]) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("INTERNAL", error.localizedDescription) + expectation.fulfill() + } + } + XCTAssert(true) + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testUnhandledErrorAsync() async { + let function = functions.httpsCallable( + "unhandledErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + do { + _ = try await function.call([]) + XCTFail("Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("INTERNAL", error.localizedDescription) + } + } + #endif + + func testUnknownError() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "unknownErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + try function.call([]) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("INTERNAL", error.localizedDescription) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testUnknownErrorAsync() async { + let function = functions.httpsCallable( + "unknownErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + do { + _ = try await function.call([]) + XCTAssertFalse(true, "Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) + XCTAssertEqual("INTERNAL", error.localizedDescription) + } + } + #endif + + func testExplicitError() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "explicitErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + try function.call([]) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.outOfRange.rawValue, error.code) + XCTAssertEqual("explicit nope", error.localizedDescription) + XCTAssertEqual(["start": 10 as Int32, "end": 20 as Int32, "long": 30], + error.userInfo[FunctionsErrorDetailsKey] as! [String: Int32]) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testExplicitErrorAsync() async { + let function = functions.httpsCallable( + "explicitErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + do { + _ = try await function.call([]) + XCTAssertFalse(true, "Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.outOfRange.rawValue, error.code) + XCTAssertEqual("explicit nope", error.localizedDescription) + XCTAssertEqual(["start": 10 as Int32, "end": 20 as Int32, "long": 30], + error.userInfo[FunctionsErrorDetailsKey] as! [String: Int32]) + } + } + #endif + + func testHttpError() throws { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "httpErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + XCTAssertNotNil(function) + try function.call([]) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.invalidArgument.rawValue, error.code) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testHttpErrorAsync() async { + let function = functions.httpsCallable( + "httpErrorTest", + requestType: [Int].self, + responseType: Int.self + ) + do { + _ = try await function.call([]) + XCTAssertFalse(true, "Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.invalidArgument.rawValue, error.code) + } + } + #endif + + func testTimeout() throws { + let expectation = expectation(description: #function) + var function = functions.httpsCallable( + "timeoutTest", + requestType: [Int].self, + responseType: Int.self + ) + function.timeoutInterval = 0.05 + try function.call([]) { result in + do { + _ = try result.get() + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.deadlineExceeded.rawValue, error.code) + XCTAssertEqual("DEADLINE EXCEEDED", error.localizedDescription) + XCTAssertNil(error.userInfo[FunctionsErrorDetailsKey]) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testTimeoutAsync() async { + var function = functions.httpsCallable( + "timeoutTest", + requestType: [Int].self, + responseType: Int.self + ) + function.timeoutInterval = 0.05 + do { + _ = try await function.call([]) + XCTAssertFalse(true, "Failed to throw error for missing result") + } catch { + let error = error as NSError + XCTAssertEqual(FunctionsErrorCode.deadlineExceeded.rawValue, error.code) + XCTAssertEqual("DEADLINE EXCEEDED", error.localizedDescription) + XCTAssertNil(error.userInfo[FunctionsErrorDetailsKey]) + } + } + #endif +} diff --git a/Package.swift b/Package.swift index f58be9fb10a..6e3669ba29e 100644 --- a/Package.swift +++ b/Package.swift @@ -91,6 +91,10 @@ let package = Package( name: "FirebaseFunctions", targets: ["FirebaseFunctions"] ), + .library( + name: "FirebaseFunctionsSwift-Beta", + targets: ["FirebaseFunctionsSwift"] + ), .library( name: "FirebaseInAppMessaging-Beta", targets: ["FirebaseInAppMessagingTarget"] @@ -677,6 +681,21 @@ let package = Package( .headerSearchPath("../../"), ] ), + .target( + name: "FirebaseFunctionsSwift", + dependencies: [ + "FirebaseFunctions", + "FirebaseSharedSwift", + ], + path: "FirebaseFunctionsSwift/Sources" + ), + .testTarget( + name: "FirebaseFunctionsSwiftUnit", + dependencies: ["FirebaseFunctionsSwift", + "FirebaseFunctionsTestingSupport", + "SharedTestUtilities"], + path: "FirebaseFunctionsSwift/Tests" + ), .target( name: "FirebaseFunctionsCombineSwift", dependencies: ["FirebaseFunctions"], From 694760bd8b11fb75a042e4af6e8cf2e0f2844008 Mon Sep 17 00:00:00 2001 From: Peter Friese Date: Mon, 6 Dec 2021 10:57:05 +0100 Subject: [PATCH 3/7] Make HPPS Callable Functions callable like a plain Swift function Example: let greeter = functions.httpsCallable("greeter", requestType: GreetingRequest.self, responseType: GreetingResponse.self) let result = try await greeter(data) print(result.greeting) Signed-off-by: Peter Friese --- .../Sources/Codable/Callable+Codable.swift | 38 +++++++++++++ .../Tests/IntegrationTests.swift | 56 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift index e6e1355d812..551c91217d9 100644 --- a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -107,6 +107,26 @@ public struct Callable { } } + /// Creates a directly callable function. + /// + /// This allows users to call a HTTPS Callable Funciton like a normal Swift function: + /// + /// let greeter = functions.httpsCallable("greeter", + /// requestType: GreetingRequest.self, + /// responseType: GreetingResponse.self) + /// try 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, + completion: @escaping (Result) + -> Void) throws { + try call(data, completion: completion) + } + #if compiler(>=5.5) && canImport(_Concurrency) /** * Executes this Callable HTTPS trigger asynchronously. @@ -138,5 +158,23 @@ public struct Callable { 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 Funciton like a normal Swift function: + /// + /// let greeter = functions.httpsCallable("greeter", + /// requestType: GreetingRequest.self, + /// responseType: GreetingResponse.self) + /// 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) async throws -> Response { + return try await call(data) + } #endif } diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift index d9880f57ef6..a650f189a68 100644 --- a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -531,4 +531,60 @@ class IntegrationTests: XCTestCase { } } #endif + + func testCallAsFunction() throws { + let expectation = expectation(description: #function) + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + let function = functions.httpsCallable("dataTest", + requestType: DataTestRequest.self, + responseType: DataTestResponse.self) + try function(data) { result in + do { + let response = try result.get() + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testCallAsFunctionAsync() async throws { + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + + let function = functions.httpsCallable("dataTest", + requestType: DataTestRequest.self, + responseType: DataTestResponse.self) + + let response = try await function(data) + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + } + #endif } From ee57ffeb7e7602881ff8a8dee1355b2034741c29 Mon Sep 17 00:00:00 2001 From: Peter Friese Date: Mon, 6 Dec 2021 11:37:00 +0100 Subject: [PATCH 4/7] Allow caller to use type inferrence when calling HTTPSCallable functions Signed-off-by: Peter Friese --- .../Sources/Codable/Callable+Codable.swift | 19 +++++++ .../Tests/IntegrationTests.swift | 54 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift index 551c91217d9..e090054a0b5 100644 --- a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -36,6 +36,25 @@ public extension Functions { -> Callable { return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder) } + + /// Creates a reference to the Callable HTTPS trigger with the given name. The types of the `Encodable` + /// request and tyhe `Decodable` response will be inferred by the compiler. + /// + /// At the call site, use the following syntax: + /// + /// let greeter: Callable = functions.httpsCallable("greeter") + /// try greeter(data) { result in + /// print(result.greeting) + /// } + /// + /// - Parameters: + /// - name: The name of the Callable HTTPS trigger + func httpsCallable(_ name: String, + encoder: StructureEncoder = StructureEncoder(), + decoder: StructureDecoder = StructureDecoder()) + -> Callable where Request: Encodable, Response: Decodable { + return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder) + } } /** diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift index a650f189a68..62e03166e77 100644 --- a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -587,4 +587,58 @@ class IntegrationTests: XCTestCase { XCTAssertEqual(response, expected) } #endif + + func testInferredTypes() throws { + let expectation = expectation(description: #function) + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + let function: Callable = functions.httpsCallable("dataTest") + + try function(data) { result in + do { + let response = try result.get() + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + expectation.fulfill() + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testInferredTyesAsync() async throws { + let data = DataTestRequest( + bool: true, + int: 2, + long: 9_876_543_210, + string: "four", + array: [5, 6], + null: nil + ) + + let function: Callable = functions + .httpsCallable("dataTest") + + let response = try await function(data) + let expected = DataTestResponse( + message: "stub response", + long: 420, + code: 42 + ) + XCTAssertEqual(response, expected) + } + #endif } From ac4df1f15a776a635e4dcdbaa10e9458b47cf176 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 18 Nov 2021 11:15:06 -0800 Subject: [PATCH 5/7] Add CI for Functions Codable and address review feedback --- .github/workflows/functions.yml | 8 +- .../Sources/Codable/EncoderDecoder.swift | 4 +- .../Codable/ServerValueCodingTests.swift | 9 - .../Sources/Codable/Callable+Codable.swift | 219 ++++++++---------- .../Tests/IntegrationTests.swift | 130 ++++++----- FirebaseSharedSwift/CHANGELOG.md | 4 +- .../FirebaseDataEncoder.swift} | 20 +- .../LICENSE | 0 .../METADATA | 0 ...s.swift => FirebaseDataEncoderTests.swift} | 24 +- ...oderTests.swift => DataEncoderTests.swift} | 112 ++++----- Package.swift | 4 +- .../FirebaseFunctionsSwiftUnit.xcscheme | 52 +++++ 13 files changed, 312 insertions(+), 274 deletions(-) rename FirebaseSharedSwift/Sources/third_party/{StructureEncoder/StructureEncoder.swift => FirebaseDataEncoder/FirebaseDataEncoder.swift} (99%) rename FirebaseSharedSwift/Sources/third_party/{StructureEncoder => FirebaseDataEncoder}/LICENSE (100%) rename FirebaseSharedSwift/Sources/third_party/{StructureEncoder => FirebaseDataEncoder}/METADATA (100%) rename FirebaseSharedSwift/Tests/Codable/{StructureEncoderTests.swift => FirebaseDataEncoderTests.swift} (94%) rename FirebaseSharedSwift/Tests/third_party/{EncoderTests.swift => DataEncoderTests.swift} (95%) create mode 100644 scripts/spm_test_schemes/FirebaseFunctionsSwiftUnit.xcscheme diff --git a/.github/workflows/functions.yml b/.github/workflows/functions.yml index b72276ffa08..3b835f9664f 100644 --- a/.github/workflows/functions.yml +++ b/.github/workflows/functions.yml @@ -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 @@ -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. diff --git a/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift b/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift index 03bf696c4e1..32083bd0867 100644 --- a/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift +++ b/FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift @@ -18,6 +18,6 @@ import FirebaseDatabase import FirebaseSharedSwift extension Database { - public typealias Encoder = StructureEncoder - public typealias Decoder = StructureDecoder + public typealias Encoder = FirebaseDataEncoder + public typealias Decoder = FirebaseDataDecoder } diff --git a/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift b/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift index 259f1eee3e4..6593011e419 100644 --- a/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift +++ b/FirebaseDatabaseSwift/Tests/Codable/ServerValueCodingTests.swift @@ -188,13 +188,4 @@ class DictionarySubject { XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line) } } - - func failsDecoding(to _: X.Type, - using decoder: Database.Decoder = .init()) -> Void { - XCTAssertThrowsError( - try decoder.decode(X.self, from: subject), - file: file, - line: line - ) - } } diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift index e090054a0b5..9d5d1b16530 100644 --- a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -1,69 +1,41 @@ -/* - * Copyright 2020 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. - */ +// 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` - */ + /// 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(_ name: String, - requestType: Request.Type, - responseType: Response.Type, - encoder: StructureEncoder = StructureEncoder(), - decoder: StructureDecoder = StructureDecoder()) + requestAs requestType: Request.Type = Request.self, + responseAs responseType: Response.Type = Response.self, + encoder: FirebaseDataEncoder = FirebaseDataEncoder(), + decoder: FirebaseDataDecoder = FirebaseDataDecoder()) -> Callable { return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder) } - - /// Creates a reference to the Callable HTTPS trigger with the given name. The types of the `Encodable` - /// request and tyhe `Decodable` response will be inferred by the compiler. - /// - /// At the call site, use the following syntax: - /// - /// let greeter: Callable = functions.httpsCallable("greeter") - /// try greeter(data) { result in - /// print(result.greeting) - /// } - /// - /// - Parameters: - /// - name: The name of the Callable HTTPS trigger - func httpsCallable(_ name: String, - encoder: StructureEncoder = StructureEncoder(), - decoder: StructureDecoder = StructureDecoder()) - -> Callable where Request: Encodable, Response: Decodable { - return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder) - } } -/** - * A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. - */ +// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. public struct Callable { - /** - * The timeout to use when calling the function. Defaults to 60 seconds. - */ + /// The timeout to use when calling the function. Defaults to 60 seconds. public var timeoutInterval: TimeInterval { get { callable.timeoutInterval @@ -78,101 +50,106 @@ public struct Callable { } private let callable: HTTPSCallable - private let encoder: StructureEncoder - private let decoder: StructureDecoder + private let encoder: FirebaseDataEncoder + private let decoder: FirebaseDataDecoder - init(callable: HTTPSCallable, encoder: StructureEncoder, decoder: StructureDecoder) { + 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 - * Firebase Instance ID 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 Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see `[FIRInstanceID deleteIDWithHandler:]`. It - * resumes with a new Instance ID 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. - * - * - Throws: An error if any value throws an error during encoding. - */ + /// 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, completion: @escaping (Result) - -> Void) throws { - let encoded = try encoder.encode(data) + -> Void) { + 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 { + 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)) - } 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 Funciton like a normal Swift function: - /// + /// This allows users to call a HTTPS Callable Function like a normal Swift function: + /// ```swift /// let greeter = functions.httpsCallable("greeter", /// requestType: GreetingRequest.self, /// responseType: GreetingResponse.self) - /// try greeter(data) { result in + /// greeter(data) { result in /// print(result.greeting) /// } - /// + /// ``` + /// You can also call a HTTPS Callable function using the following syntax: + /// ```swift + /// let greeter: Callable = 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, completion: @escaping (Result) - -> Void) throws { - try call(data, completion: completion) + -> 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 - * Firebase Instance ID 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 Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see `[FIRInstanceID deleteIDWithHandler:]`. It - * resumes with a new Instance ID 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 - */ + /// 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, - encoder: StructureEncoder = StructureEncoder(), - decoder: StructureDecoder = - StructureDecoder()) async throws -> Response { + 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) @@ -180,14 +157,20 @@ public struct Callable { /// Creates a directly callable function. /// - /// This allows users to call a HTTPS Callable Funciton like a normal Swift function: - /// + /// This allows users to call a HTTPS Callable Function like a normal Swift function: + /// ```swift /// let greeter = functions.httpsCallable("greeter", /// 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 = 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 diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift index 62e03166e77..9b707704835 100644 --- a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -86,9 +86,9 @@ class IntegrationTests: XCTestCase { null: nil ) let function = functions.httpsCallable("dataTest", - requestType: DataTestRequest.self, - responseType: DataTestResponse.self) - try function.call(data) { result in + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) + function.call(data) { result in do { let response = try result.get() let expected = DataTestResponse( @@ -118,8 +118,8 @@ class IntegrationTests: XCTestCase { ) let function = functions.httpsCallable("dataTest", - requestType: DataTestRequest.self, - responseType: DataTestResponse.self) + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) let response = try await function.call(data) let expected = DataTestResponse( @@ -135,10 +135,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "scalarTest", - requestType: Int16.self, - responseType: Int.self + requestAs: Int16.self, + responseAs: Int.self ) - try function.call(17) { result in + function.call(17) { result in do { let response = try result.get() XCTAssertEqual(response, 76) @@ -155,13 +155,21 @@ class IntegrationTests: XCTestCase { func testScalarAsync() async throws { let function = functions.httpsCallable( "scalarTest", - requestType: Int16.self, - responseType: Int.self + requestAs: Int16.self, + responseAs: Int.self ) let result = try await function.call(17) XCTAssertEqual(result, 76) } + + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testScalarAsyncAlternateSignature() async throws { + let function: Callable = functions.httpsCallable("scalarTest") + let result = try await function.call(17) + XCTAssertEqual(result, 76) + } + #endif func testToken() throws { @@ -177,11 +185,11 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "FCMTokenTest", - requestType: [String: Int].self, - responseType: [String: Int].self + requestAs: [String: Int].self, + responseAs: [String: Int].self ) XCTAssertNotNil(function) - try function.call([:]) { result in + function.call([:]) { result in do { let data = try result.get() XCTAssertEqual(data, [:]) @@ -207,8 +215,8 @@ class IntegrationTests: XCTestCase { let function = functions.httpsCallable( "FCMTokenTest", - requestType: [String: Int].self, - responseType: [String: Int].self + requestAs: [String: Int].self, + responseAs: [String: Int].self ) let data = try await function.call([:]) @@ -220,10 +228,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "FCMTokenTest", - requestType: [String: Int].self, - responseType: [String: Int].self + requestAs: [String: Int].self, + responseAs: [String: Int].self ) - try function.call([:]) { result in + function.call([:]) { result in do { let data = try result.get() XCTAssertEqual(data, [:]) @@ -240,8 +248,8 @@ class IntegrationTests: XCTestCase { func testFCMTokenAsync() async throws { let function = functions.httpsCallable( "FCMTokenTest", - requestType: [String: Int].self, - responseType: [String: Int].self + requestAs: [String: Int].self, + responseAs: [String: Int].self ) let data = try await function.call([:]) @@ -253,10 +261,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "nullTest", - requestType: Int?.self, - responseType: Int?.self + requestAs: Int?.self, + responseAs: Int?.self ) - try function.call(nil) { result in + function.call(nil) { result in do { let data = try result.get() XCTAssertEqual(data, nil) @@ -273,8 +281,8 @@ class IntegrationTests: XCTestCase { func testNullAsync() async throws { let function = functions.httpsCallable( "nullTest", - requestType: Int?.self, - responseType: Int?.self + requestAs: Int?.self, + responseAs: Int?.self ) let data = try await function.call(nil) @@ -294,10 +302,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "missingResultTest", - requestType: Int?.self, - responseType: Int?.self + requestAs: Int?.self, + responseAs: Int?.self ) - try function.call(nil) { result in + function.call(nil) { result in do { _ = try result.get() } catch { @@ -315,8 +323,8 @@ class IntegrationTests: XCTestCase { func testMissingResultAsync() async { let function = functions.httpsCallable( "missingResultTest", - requestType: Int?.self, - responseType: Int?.self + requestAs: Int?.self, + responseAs: Int?.self ) do { _ = try await function.call(nil) @@ -333,10 +341,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "unhandledErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) - try function.call([]) { result in + function.call([]) { result in do { _ = try result.get() } catch { @@ -355,8 +363,8 @@ class IntegrationTests: XCTestCase { func testUnhandledErrorAsync() async { let function = functions.httpsCallable( "unhandledErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) do { _ = try await function.call([]) @@ -373,10 +381,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "unknownErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) - try function.call([]) { result in + function.call([]) { result in do { _ = try result.get() } catch { @@ -394,8 +402,8 @@ class IntegrationTests: XCTestCase { func testUnknownErrorAsync() async { let function = functions.httpsCallable( "unknownErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) do { _ = try await function.call([]) @@ -412,10 +420,10 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "explicitErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) - try function.call([]) { result in + function.call([]) { result in do { _ = try result.get() } catch { @@ -435,8 +443,8 @@ class IntegrationTests: XCTestCase { func testExplicitErrorAsync() async { let function = functions.httpsCallable( "explicitErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) do { _ = try await function.call([]) @@ -455,11 +463,11 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) let function = functions.httpsCallable( "httpErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) XCTAssertNotNil(function) - try function.call([]) { result in + function.call([]) { result in do { _ = try result.get() } catch { @@ -476,8 +484,8 @@ class IntegrationTests: XCTestCase { func testHttpErrorAsync() async { let function = functions.httpsCallable( "httpErrorTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) do { _ = try await function.call([]) @@ -493,11 +501,11 @@ class IntegrationTests: XCTestCase { let expectation = expectation(description: #function) var function = functions.httpsCallable( "timeoutTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) function.timeoutInterval = 0.05 - try function.call([]) { result in + function.call([]) { result in do { _ = try result.get() } catch { @@ -516,8 +524,8 @@ class IntegrationTests: XCTestCase { func testTimeoutAsync() async { var function = functions.httpsCallable( "timeoutTest", - requestType: [Int].self, - responseType: Int.self + requestAs: [Int].self, + responseAs: Int.self ) function.timeoutInterval = 0.05 do { @@ -543,9 +551,9 @@ class IntegrationTests: XCTestCase { null: nil ) let function = functions.httpsCallable("dataTest", - requestType: DataTestRequest.self, - responseType: DataTestResponse.self) - try function(data) { result in + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) + function(data) { result in do { let response = try result.get() let expected = DataTestResponse( @@ -575,8 +583,8 @@ class IntegrationTests: XCTestCase { ) let function = functions.httpsCallable("dataTest", - requestType: DataTestRequest.self, - responseType: DataTestResponse.self) + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) let response = try await function(data) let expected = DataTestResponse( @@ -600,7 +608,7 @@ class IntegrationTests: XCTestCase { ) let function: Callable = functions.httpsCallable("dataTest") - try function(data) { result in + function(data) { result in do { let response = try result.get() let expected = DataTestResponse( diff --git a/FirebaseSharedSwift/CHANGELOG.md b/FirebaseSharedSwift/CHANGELOG.md index 8b137891791..f39a7aa6fed 100644 --- a/FirebaseSharedSwift/CHANGELOG.md +++ b/FirebaseSharedSwift/CHANGELOG.md @@ -1 +1,3 @@ - +# 8.11.0-beta +- Introduced shared Codable implementation. Initially used by Firebase Functions + and Firebase Database. (#9091) diff --git a/FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift similarity index 99% rename from FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift rename to FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift index c6aec8351b1..c5e69ac6e89 100644 --- a/FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift +++ b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift @@ -95,7 +95,7 @@ extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, // used in the new runtime. _TtC10Foundation13__JSONEncoder is the // mangled name for Foundation.__JSONEncoder. -public class StructureEncoder { +public class FirebaseDataEncoder { // MARK: Options /// The strategy to use for encoding `Date` values. @@ -290,7 +290,7 @@ fileprivate class __JSONEncoder : Encoder { fileprivate var storage: _JSONEncodingStorage /// Options set on the top-level encoder. - fileprivate let options: StructureEncoder._Options + fileprivate let options: FirebaseDataEncoder._Options /// The path to the current point in encoding. public var codingPath: [CodingKey] @@ -303,7 +303,7 @@ fileprivate class __JSONEncoder : Encoder { // MARK: - Initialization /// Initializes `self` with the given top-level encoder options. - fileprivate init(options: StructureEncoder._Options, codingPath: [CodingKey] = []) { + fileprivate init(options: FirebaseDataEncoder._Options, codingPath: [CodingKey] = []) { self.options = options self.storage = _JSONEncodingStorage() self.codingPath = codingPath @@ -437,7 +437,7 @@ fileprivate struct _JSONKeyedEncodingContainer : KeyedEncodingCon case .useDefaultKeys: return key case .convertToSnakeCase: - let newKeyString = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue) + let newKeyString = FirebaseDataEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue) return _JSONKey(stringValue: newKeyString, intValue: key.intValue) case .custom(let converter): return converter(codingPath + [key]) @@ -1039,7 +1039,7 @@ fileprivate class __JSONReferencingEncoder : __JSONEncoder { // The two must coexist, so it was renamed. The old name must not be // used in the new runtime. _TtC10Foundation13__JSONDecoder is the // mangled name for Foundation.__JSONDecoder. -public class StructureDecoder { +public class FirebaseDataDecoder { // MARK: Options /// The strategy to use for decoding `Date` values. @@ -1223,7 +1223,7 @@ fileprivate class __JSONDecoder : Decoder { fileprivate var storage: _JSONDecodingStorage /// Options set on the top-level decoder. - fileprivate let options: StructureDecoder._Options + fileprivate let options: FirebaseDataDecoder._Options /// The path to the current point in encoding. fileprivate(set) public var codingPath: [CodingKey] @@ -1236,7 +1236,7 @@ fileprivate class __JSONDecoder : Decoder { // MARK: - Initialization /// Initializes `self` with the given top-level container and options. - fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: StructureDecoder._Options) { + fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: FirebaseDataDecoder._Options) { self.storage = _JSONDecodingStorage() self.storage.push(container: container) self.codingPath = codingPath @@ -1342,7 +1342,7 @@ fileprivate struct _JSONKeyedDecodingContainer : KeyedDecodingCon // Convert the snake case keys in the container to camel case. // If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with JSON dictionaries. self.container = Dictionary(container.map { - key, value in (StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) + key, value in (FirebaseDataDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) }, uniquingKeysWith: { (first, _) in first }) case .custom(let converter): self.container = Dictionary(container.map { @@ -1367,8 +1367,8 @@ fileprivate struct _JSONKeyedDecodingContainer : KeyedDecodingCon case .convertFromSnakeCase: // In this case we can attempt to recover the original value by reversing the transform let original = key.stringValue - let converted = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(original) - let roundtrip = StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(converted) + let converted = FirebaseDataEncoder.KeyEncodingStrategy._convertToSnakeCase(original) + let roundtrip = FirebaseDataDecoder.KeyDecodingStrategy._convertFromSnakeCase(converted) if converted == original { return "\(key) (\"\(original)\")" } else if roundtrip == original { diff --git a/FirebaseSharedSwift/Sources/third_party/StructureEncoder/LICENSE b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/LICENSE similarity index 100% rename from FirebaseSharedSwift/Sources/third_party/StructureEncoder/LICENSE rename to FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/LICENSE diff --git a/FirebaseSharedSwift/Sources/third_party/StructureEncoder/METADATA b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/METADATA similarity index 100% rename from FirebaseSharedSwift/Sources/third_party/StructureEncoder/METADATA rename to FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/METADATA diff --git a/FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift b/FirebaseSharedSwift/Tests/Codable/FirebaseDataEncoderTests.swift similarity index 94% rename from FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift rename to FirebaseSharedSwift/Tests/Codable/FirebaseDataEncoderTests.swift index cecf1d1c1d0..c8450c567ce 100644 --- a/FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift +++ b/FirebaseSharedSwift/Tests/Codable/FirebaseDataEncoderTests.swift @@ -18,7 +18,7 @@ import Foundation import FirebaseSharedSwift import XCTest -class FirebaseStructureEncoderTests: XCTestCase { +class FirebaseFirebaseDataEncoderTests: XCTestCase { func testInt() { struct Model: Codable, Equatable { let x: Int @@ -29,7 +29,7 @@ class FirebaseStructureEncoderTests: XCTestCase { } func testNullDecodesAsNil() throws { - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() let opt = try decoder.decode(Int?.self, from: NSNull()) XCTAssertNil(opt) } @@ -52,9 +52,9 @@ class FirebaseStructureEncoderTests: XCTestCase { } let model = Model(snakeCase: 42) let dict = ["snake_case": 42] - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase assertThat(model).roundTrips(to: dict, using: encoder, decoder: decoder) } @@ -418,14 +418,14 @@ class EncodableSubject { @discardableResult func encodes(to expected: [String: Any], - using encoder: StructureEncoder = .init()) -> DictionarySubject { + using encoder: FirebaseDataEncoder = .init()) -> DictionarySubject { let encoded = assertEncodes(to: expected, using: encoder) return DictionarySubject(encoded, file: file, line: line) } func failsToEncode() { do { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase _ = try encoder.encode(subject) } catch { @@ -436,7 +436,7 @@ class EncodableSubject { func failsEncodingAtTopLevel() { do { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase _ = try encoder.encode(subject) XCTFail("Failed to throw", file: file, line: line) @@ -448,7 +448,7 @@ class EncodableSubject { } private func assertEncodes(to expected: [String: Any], - using encoder: StructureEncoder = .init()) -> [String: Any] { + using encoder: FirebaseDataEncoder = .init()) -> [String: Any] { do { let enc = try encoder.encode(subject) XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line) @@ -462,8 +462,8 @@ class EncodableSubject { class CodableSubject: EncodableSubject { func roundTrips(to expected: [String: Any], - using encoder: StructureEncoder = .init(), - decoder: StructureDecoder = .init()) { + using encoder: FirebaseDataEncoder = .init(), + decoder: FirebaseDataDecoder = .init()) { let reverseSubject = encodes(to: expected, using: encoder) reverseSubject.decodes(to: subject, using: decoder) } @@ -481,7 +481,7 @@ class DictionarySubject { } func decodes(to expected: X, - using decoder: StructureDecoder = .init()) -> Void { + using decoder: FirebaseDataDecoder = .init()) -> Void { do { let decoded = try decoder.decode(X.self, from: subject) XCTAssertEqual(decoded, expected) @@ -491,7 +491,7 @@ class DictionarySubject { } func failsDecoding(to _: X.Type, - using decoder: StructureDecoder = .init()) -> Void { + using decoder: FirebaseDataDecoder = .init()) -> Void { XCTAssertThrowsError( try decoder.decode(X.self, from: subject), file: file, diff --git a/FirebaseSharedSwift/Tests/third_party/EncoderTests.swift b/FirebaseSharedSwift/Tests/third_party/DataEncoderTests.swift similarity index 95% rename from FirebaseSharedSwift/Tests/third_party/EncoderTests.swift rename to FirebaseSharedSwift/Tests/third_party/DataEncoderTests.swift index e088baab3d3..90f23b9850a 100644 --- a/FirebaseSharedSwift/Tests/third_party/EncoderTests.swift +++ b/FirebaseSharedSwift/Tests/third_party/DataEncoderTests.swift @@ -16,7 +16,7 @@ import Foundation import XCTest -class TestStructureEncoder: XCTestCase { +class TestFirebaseDataEncoder: XCTestCase { // MARK: - Encoding Top-Level Empty Types func testEncodingTopLevelEmptyStruct() { @@ -215,14 +215,14 @@ class TestStructureEncoder: XCTestCase { func localTestRoundTrip(of value: T) { var payload: Any! = nil do { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() payload = try encoder.encode(value) } catch { XCTFail("Failed to encode \(T.self): \(error)") } do { - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() let decoded = try decoder.decode(T.self, from: payload!) /// `snprintf`'s `%g`, which `JSONSerialization` uses internally for double values, does not respect @@ -474,12 +474,12 @@ class TestStructureEncoder: XCTestCase { } func testEncodingNonConformingFloatStrings() { - let encodingStrategy: StructureEncoder.NonConformingFloatEncodingStrategy = .convertToString( + let encodingStrategy: FirebaseDataEncoder.NonConformingFloatEncodingStrategy = .convertToString( positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN" ) - let decodingStrategy: StructureDecoder.NonConformingFloatDecodingStrategy = .convertFromString( + let decodingStrategy: FirebaseDataDecoder.NonConformingFloatDecodingStrategy = .convertFromString( positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN" @@ -581,7 +581,7 @@ class TestStructureEncoder: XCTestCase { let expected = ["\(test.1)": "test"] let encoded = EncodeMe(keyName: test.0) - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(encoded) @@ -593,7 +593,7 @@ class TestStructureEncoder: XCTestCase { let expected = ["QQQhello": "test"] let encoded = EncodeMe(keyName: "hello") - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)! return key @@ -607,7 +607,7 @@ class TestStructureEncoder: XCTestCase { func testEncodingDictionaryStringKeyConversionUntouched() { let toEncode = ["leaveMeAlone": "test"] - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(toEncode) @@ -625,7 +625,7 @@ class TestStructureEncoder: XCTestCase { func testEncodingDictionaryFailureKeyPath() { let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)] - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -642,7 +642,7 @@ class TestStructureEncoder: XCTestCase { let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]] - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -672,7 +672,7 @@ class TestStructureEncoder: XCTestCase { let encoded = EncodeNestedNested(outerValue: EncodeNested(nestedValue: EncodeMe(keyName: "helloWorld"))) - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() var callCount = 0 let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in @@ -757,7 +757,7 @@ class TestStructureEncoder: XCTestCase { // This structure contains the camel case key that the test object should decode with, then it uses the snake case key (test.0) as the actual key for the boolean value. let input = ["camelCaseKey": "\(test.1)", "\(test.0)": true] as [String: Any] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode(DecodeMe.self, from: input) @@ -770,7 +770,7 @@ class TestStructureEncoder: XCTestCase { func testDecodingKeyStrategyCustom() { let input = ["----hello": "test"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in // This converter removes the first 4 characters from the start of all string keys, if it has more than 4 characters let string = path.last!.stringValue @@ -786,7 +786,7 @@ class TestStructureEncoder: XCTestCase { func testDecodingDictionaryStringKeyConversionUntouched() { let input = ["leave_me_alone": "test"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode([String: String].self, from: input) @@ -795,7 +795,7 @@ class TestStructureEncoder: XCTestCase { func testDecodingDictionaryFailureKeyPath() { let input = ["leave_me_alone": "test"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { _ = try decoder.decode([String: Int].self, from: input) @@ -817,7 +817,7 @@ class TestStructureEncoder: XCTestCase { func testDecodingDictionaryFailureKeyPathNested() { let input = ["top_level": ["sub_level": ["nested_value": ["int_value": "not_an_int"]]]] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { _ = try decoder.decode([String: [String: DecodeFailureNested]].self, from: input) @@ -839,7 +839,7 @@ class TestStructureEncoder: XCTestCase { func testEncodingKeyStrategySnakeGenerated() { // Test that this works with a struct that has automatically generated keys let input = ["this_is_camel_case": "test"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try! decoder.decode(DecodeMe3.self, from: input) @@ -848,7 +848,7 @@ class TestStructureEncoder: XCTestCase { func testDecodingKeyStrategyCamelGenerated() { let encoded = DecodeMe3(thisIsCamelCase: "test") - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let result = try! encoder.encode(encoded) XCTAssertEqual(["this_is_camel_case": "test"], result as? [String: String]) @@ -867,7 +867,7 @@ class TestStructureEncoder: XCTestCase { // Decoding let input = ["foo_bar": "test", "this_is_camel_case_too": "test2"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let decodingResult = try! decoder.decode(DecodeMe4.self, from: input) @@ -876,7 +876,7 @@ class TestStructureEncoder: XCTestCase { // Encoding let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2") - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let encodingResult = try! encoder.encode(encoded) XCTAssertEqual( @@ -922,7 +922,7 @@ class TestStructureEncoder: XCTestCase { // Decoding // This input has a dictionary with two keys, but only one will end up in the container let input = ["unused key 1": "test1", "unused key 2": "test2"] - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.keyDecodingStrategy = .custom(customKeyConversion) let decodingResult = try! decoder.decode(DecodeMe5.self, from: input) @@ -931,7 +931,7 @@ class TestStructureEncoder: XCTestCase { // Encoding let encoded = DecodeMe5() - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.keyEncodingStrategy = .custom(customKeyConversion) let decodingResult2 = try! encoder.encode(encoded) @@ -942,7 +942,7 @@ class TestStructureEncoder: XCTestCase { // MARK: - Encoder Features func testNestedContainerCodingPaths() { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() do { _ = try encoder.encode(NestedContainersTestType()) } catch let error as NSError { @@ -951,7 +951,7 @@ class TestStructureEncoder: XCTestCase { } func testSuperEncoderCodingPaths() { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() do { _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } catch let error as NSError { @@ -975,7 +975,7 @@ class TestStructureEncoder: XCTestCase { } func testInterceptURL() { - // Want to make sure StructureEncoder writes out single-value URLs, not the keyed encoding. + // Want to make sure FirebaseDataEncoder writes out single-value URLs, not the keyed encoding. let expected = "http://swift.org" let url = URL(string: "http://swift.org")! _testRoundTrip(of: url, expected: expected) @@ -1014,13 +1014,13 @@ class TestStructureEncoder: XCTestCase { } func testDecodingConcreteTypeParameter() { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() guard let value = try? encoder.encode(Employee.testValue) else { XCTFail("Unable to encode Employee.") return } - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() guard let decoded = try? decoder.decode(Employee.self as Person.Type, from: value) else { XCTFail("Failed to decode Employee as Person.") return @@ -1061,7 +1061,7 @@ class TestStructureEncoder: XCTestCase { // // The issue at hand reproduces when you have a referencing encoder (superEncoder() creates one) that has a container on the stack (unkeyedContainer() adds one) that encodes a value going through box_() (Array does that) that encodes something which throws (Float.infinity does that). // When reproducing, this will cause a test failure via fatalError(). - _ = try? StructureEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) + _ = try? FirebaseDataEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) } func testEncoderStateThrowOnEncodeCustomDate() { @@ -1078,7 +1078,7 @@ class TestStructureEncoder: XCTestCase { } // The closure needs to push a container before throwing an error to trigger. - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.dateEncodingStrategy = .custom { _, encoder in _ = encoder.unkeyedContainer() enum CustomError: Error { case foo } @@ -1102,7 +1102,7 @@ class TestStructureEncoder: XCTestCase { } // The closure needs to push a container before throwing an error to trigger. - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.dataEncodingStrategy = .custom { _, encoder in _ = encoder.unkeyedContainer() enum CustomError: Error { case foo } @@ -1120,12 +1120,12 @@ class TestStructureEncoder: XCTestCase { // Once Array decoding begins, 1 is pushed onto the container stack ([[1,2,3], 1]), and 1 is attempted to be decoded as String. This throws a .typeMismatch, but the container is not popped off the stack. // When attempting to decode [Int], the container stack is still ([[1,2,3], 1]), and 1 fails to decode as [Int]. let input = [1, 2, 3] - _ = try! StructureDecoder().decode(EitherDecodable<[String], [Int]>.self, from: input) + _ = try! FirebaseDataDecoder().decode(EitherDecodable<[String], [Int]>.self, from: input) } func testDecoderStateThrowOnDecodeCustomDate() { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.dateDecodingStrategy = .custom { decoder in enum CustomError: Error { case foo } throw CustomError.foo @@ -1137,7 +1137,7 @@ class TestStructureEncoder: XCTestCase { func testDecoderStateThrowOnDecodeCustomData() { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.dataDecodingStrategy = .custom { decoder in enum CustomError: Error { case foo } throw CustomError.foo @@ -1155,34 +1155,34 @@ class TestStructureEncoder: XCTestCase { private func _testEncodeFailure(of value: T) { do { - _ = try StructureEncoder().encode(value) + _ = try FirebaseDataEncoder().encode(value) XCTFail("Encode of top-level \(T.self) was expected to fail.") } catch {} } private func _testRoundTrip(of value: T, expected: U, - dateEncodingStrategy: StructureEncoder + dateEncodingStrategy: FirebaseDataEncoder .DateEncodingStrategy = .deferredToDate, - dateDecodingStrategy: StructureDecoder + dateDecodingStrategy: FirebaseDataDecoder .DateDecodingStrategy = .deferredToDate, - dataEncodingStrategy: StructureEncoder + dataEncodingStrategy: FirebaseDataEncoder .DataEncodingStrategy = .base64, - dataDecodingStrategy: StructureDecoder + dataDecodingStrategy: FirebaseDataDecoder .DataDecodingStrategy = .base64, - keyEncodingStrategy: StructureEncoder + keyEncodingStrategy: FirebaseDataEncoder .KeyEncodingStrategy = .useDefaultKeys, - keyDecodingStrategy: StructureDecoder + keyDecodingStrategy: FirebaseDataDecoder .KeyDecodingStrategy = .useDefaultKeys, - nonConformingFloatEncodingStrategy: StructureEncoder + nonConformingFloatEncodingStrategy: FirebaseDataEncoder .NonConformingFloatEncodingStrategy = .throw, - nonConformingFloatDecodingStrategy: StructureDecoder + nonConformingFloatDecodingStrategy: FirebaseDataDecoder .NonConformingFloatDecodingStrategy = .throw) where T: Codable, T: Equatable, U: Equatable { var payload: Any! = nil do { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy @@ -1199,7 +1199,7 @@ class TestStructureEncoder: XCTestCase { ) do { - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy decoder.dataDecodingStrategy = dataDecodingStrategy decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy @@ -1212,26 +1212,26 @@ class TestStructureEncoder: XCTestCase { } private func _testRoundTrip(of value: T, - dateEncodingStrategy: StructureEncoder + dateEncodingStrategy: FirebaseDataEncoder .DateEncodingStrategy = .deferredToDate, - dateDecodingStrategy: StructureDecoder + dateDecodingStrategy: FirebaseDataDecoder .DateDecodingStrategy = .deferredToDate, - dataEncodingStrategy: StructureEncoder + dataEncodingStrategy: FirebaseDataEncoder .DataEncodingStrategy = .base64, - dataDecodingStrategy: StructureDecoder + dataDecodingStrategy: FirebaseDataDecoder .DataDecodingStrategy = .base64, - keyEncodingStrategy: StructureEncoder + keyEncodingStrategy: FirebaseDataEncoder .KeyEncodingStrategy = .useDefaultKeys, - keyDecodingStrategy: StructureDecoder + keyDecodingStrategy: FirebaseDataDecoder .KeyDecodingStrategy = .useDefaultKeys, - nonConformingFloatEncodingStrategy: StructureEncoder + nonConformingFloatEncodingStrategy: FirebaseDataEncoder .NonConformingFloatEncodingStrategy = .throw, - nonConformingFloatDecodingStrategy: StructureDecoder + nonConformingFloatDecodingStrategy: FirebaseDataDecoder .NonConformingFloatDecodingStrategy = .throw) where T: Codable, T: Equatable { var payload: Any! = nil do { - let encoder = StructureEncoder() + let encoder = FirebaseDataEncoder() encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy @@ -1242,7 +1242,7 @@ class TestStructureEncoder: XCTestCase { } do { - let decoder = StructureDecoder() + let decoder = FirebaseDataDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy decoder.dataDecodingStrategy = dataDecodingStrategy decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy @@ -1257,8 +1257,8 @@ class TestStructureEncoder: XCTestCase { private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T: Codable, U: Codable { do { - let data = try StructureEncoder().encode(value) - _ = try StructureDecoder().decode(U.self, from: data) + let data = try FirebaseDataEncoder().encode(value) + _ = try FirebaseDataDecoder().decode(U.self, from: data) XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.") } catch {} } diff --git a/Package.swift b/Package.swift index 6e3669ba29e..0cebaf56fba 100644 --- a/Package.swift +++ b/Package.swift @@ -532,8 +532,8 @@ let package = Package( name: "FirebaseSharedSwift", path: "FirebaseSharedSwift/Sources", exclude: [ - "third_party/StructureEncoder/LICENSE", - "third_party/StructureEncoder/METADATA", + "third_party/FirebaseDataEncoder/LICENSE", + "third_party/FirebaseDataEncoder/METADATA", ] ), .testTarget( diff --git a/scripts/spm_test_schemes/FirebaseFunctionsSwiftUnit.xcscheme b/scripts/spm_test_schemes/FirebaseFunctionsSwiftUnit.xcscheme new file mode 100644 index 00000000000..b8aeda528c0 --- /dev/null +++ b/scripts/spm_test_schemes/FirebaseFunctionsSwiftUnit.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + From f80e467c44227750809c6ae924f886f1402f64cd Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 15 Dec 2021 14:11:53 -0800 Subject: [PATCH 6/7] Review updates --- .../SwiftIntegration/IntegrationTests.swift | 2 +- .../Sources/Codable/Callable+Codable.swift | 8 +- .../Tests/IntegrationTests.swift | 89 ++++++++++++++----- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/FirebaseFunctions/Tests/SwiftIntegration/IntegrationTests.swift b/FirebaseFunctions/Tests/SwiftIntegration/IntegrationTests.swift index beef8782003..4c9479026dc 100644 --- a/FirebaseFunctions/Tests/SwiftIntegration/IntegrationTests.swift +++ b/FirebaseFunctions/Tests/SwiftIntegration/IntegrationTests.swift @@ -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` diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift index 9d5d1b16530..77832ed058f 100644 --- a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -73,7 +73,7 @@ public struct Callable { /// /// - 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, + public func call(_ data: Request? = nil, completion: @escaping (Result) -> Void) { do { @@ -119,7 +119,7 @@ public struct Callable { /// - Parameters: /// - data: Parameters to pass to the trigger. /// - completion: The block to call when the HTTPS request has completed. - public func callAsFunction(_ data: Request, + public func callAsFunction(_ data: Request? = nil, completion: @escaping (Result) -> Void) { call(data, completion: completion) @@ -146,7 +146,7 @@ public struct Callable { /// /// - Returns: The decoded `Response` value @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) - public func call(_ data: Request, + public func call(_ data: Request? = nil, encoder: FirebaseDataEncoder = FirebaseDataEncoder(), decoder: FirebaseDataDecoder = FirebaseDataDecoder()) async throws -> Response { @@ -175,7 +175,7 @@ public struct Callable { /// - 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) async throws -> Response { + public func callAsFunction(_ data: Request? = nil) async throws -> Response { return try await call(data) } #endif diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift index 9b707704835..a4a312e17f9 100644 --- a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -19,8 +19,8 @@ import FirebaseFunctionsSwift import FirebaseFunctionsTestingSupport import XCTest -/// This file was intitialized as a direct port of the Objective C -/// FirebaseFunctions/Tests/Integration/FIRIntegrationTests.m +/// This file was intitialized as a direct port of `FirebaseFunctionsSwift/Tests/IntegrationTests.swift` +/// which itself was ported from the Objective C `FirebaseFunctions/Tests/Integration/FIRIntegrationTests.m` /// /// The tests require the emulator to be running with `FirebaseFunctions/Backend/start.sh synchronous` /// The Firebase Functions called in the tests are implemented in `FirebaseFunctions/Backend/index.js`. @@ -75,7 +75,7 @@ class IntegrationTests: XCTestCase { functions.useLocalhost() } - func testData() throws { + func testData() { let expectation = expectation(description: #function) let data = DataTestRequest( bool: true, @@ -97,10 +97,10 @@ class IntegrationTests: XCTestCase { code: 42 ) XCTAssertEqual(response, expected) - expectation.fulfill() } catch { XCTAssert(false, "Failed to unwrap the function result: \(error)") } + expectation.fulfill() } waitForExpectations(timeout: 5) } @@ -131,7 +131,7 @@ class IntegrationTests: XCTestCase { } #endif - func testScalar() throws { + func testScalar() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "scalarTest", @@ -142,10 +142,10 @@ class IntegrationTests: XCTestCase { do { let response = try result.get() XCTAssertEqual(response, 76) - expectation.fulfill() } catch { XCTAssert(false, "Failed to unwrap the function result: \(error)") } + expectation.fulfill() } waitForExpectations(timeout: 5) } @@ -172,7 +172,7 @@ class IntegrationTests: XCTestCase { #endif - func testToken() throws { + func testToken() { // Recreate functions with a token. let functions = FunctionsFake( projectID: "functions-integration-test", @@ -193,10 +193,10 @@ class IntegrationTests: XCTestCase { do { let data = try result.get() XCTAssertEqual(data, [:]) - expectation.fulfill() } catch { XCTAssert(false, "Failed to unwrap the function result: \(error)") } + expectation.fulfill() } waitForExpectations(timeout: 5) } @@ -224,7 +224,7 @@ class IntegrationTests: XCTestCase { } #endif - func testFCMToken() throws { + func testFCMToken() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "FCMTokenTest", @@ -235,10 +235,10 @@ class IntegrationTests: XCTestCase { do { let data = try result.get() XCTAssertEqual(data, [:]) - expectation.fulfill() } catch { XCTAssert(false, "Failed to unwrap the function result: \(error)") } + expectation.fulfill() } waitForExpectations(timeout: 5) } @@ -257,7 +257,7 @@ class IntegrationTests: XCTestCase { } #endif - func testNull() throws { + func testNull() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "nullTest", @@ -268,10 +268,10 @@ class IntegrationTests: XCTestCase { do { let data = try result.get() XCTAssertEqual(data, nil) - expectation.fulfill() } catch { XCTAssert(false, "Failed to unwrap the function result: \(error)") } + expectation.fulfill() } waitForExpectations(timeout: 5) } @@ -294,11 +294,40 @@ class IntegrationTests: XCTestCase { // If no parameters are required, then the non-typed API // is more appropriate since it specifically avoids defining // type. - // func testParameterless() { - // } - // - // - func testMissingResult() throws { + func testParameterless() { + let expectation = expectation(description: #function) + let function = functions.httpsCallable( + "nullTest", + requestAs: Int?.self, + responseAs: Int?.self + ) + function.call { result in + do { + let data = try result.get() + XCTAssertEqual(data, nil) + } catch { + XCTAssert(false, "Failed to unwrap the function result: \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 5) + } + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) + func testParameterlessAsync() async throws { + let function = functions.httpsCallable( + "nullTest", + requestAs: Int?.self, + responseAs: Int?.self + ) + + let data = try await function.call() + XCTAssertEqual(data, nil) + } + #endif + + func testMissingResult() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "missingResultTest", @@ -313,7 +342,9 @@ class IntegrationTests: XCTestCase { XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) XCTAssertEqual("Response is missing data field.", error.localizedDescription) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } waitForExpectations(timeout: 5) } @@ -337,7 +368,7 @@ class IntegrationTests: XCTestCase { } #endif - func testUnhandledError() throws { + func testUnhandledError() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "unhandledErrorTest", @@ -352,7 +383,9 @@ class IntegrationTests: XCTestCase { XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) XCTAssertEqual("INTERNAL", error.localizedDescription) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } XCTAssert(true) waitForExpectations(timeout: 5) @@ -377,7 +410,7 @@ class IntegrationTests: XCTestCase { } #endif - func testUnknownError() throws { + func testUnknownError() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "unknownErrorTest", @@ -392,7 +425,9 @@ class IntegrationTests: XCTestCase { XCTAssertEqual(FunctionsErrorCode.internal.rawValue, error.code) XCTAssertEqual("INTERNAL", error.localizedDescription) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } waitForExpectations(timeout: 5) } @@ -416,7 +451,7 @@ class IntegrationTests: XCTestCase { } #endif - func testExplicitError() throws { + func testExplicitError() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "explicitErrorTest", @@ -433,7 +468,9 @@ class IntegrationTests: XCTestCase { XCTAssertEqual(["start": 10 as Int32, "end": 20 as Int32, "long": 30], error.userInfo[FunctionsErrorDetailsKey] as! [String: Int32]) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } waitForExpectations(timeout: 5) } @@ -459,7 +496,7 @@ class IntegrationTests: XCTestCase { } #endif - func testHttpError() throws { + func testHttpError() { let expectation = expectation(description: #function) let function = functions.httpsCallable( "httpErrorTest", @@ -474,7 +511,9 @@ class IntegrationTests: XCTestCase { let error = error as NSError XCTAssertEqual(FunctionsErrorCode.invalidArgument.rawValue, error.code) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } waitForExpectations(timeout: 5) } @@ -497,7 +536,7 @@ class IntegrationTests: XCTestCase { } #endif - func testTimeout() throws { + func testTimeout() { let expectation = expectation(description: #function) var function = functions.httpsCallable( "timeoutTest", @@ -514,7 +553,9 @@ class IntegrationTests: XCTestCase { XCTAssertEqual("DEADLINE EXCEEDED", error.localizedDescription) XCTAssertNil(error.userInfo[FunctionsErrorDetailsKey]) expectation.fulfill() + return } + XCTFail("Failed to throw error for missing result") } waitForExpectations(timeout: 5) } @@ -540,7 +581,7 @@ class IntegrationTests: XCTestCase { } #endif - func testCallAsFunction() throws { + func testCallAsFunction() { let expectation = expectation(description: #function) let data = DataTestRequest( bool: true, @@ -596,7 +637,7 @@ class IntegrationTests: XCTestCase { } #endif - func testInferredTypes() throws { + func testInferredTypes() { let expectation = expectation(description: #function) let data = DataTestRequest( bool: true, From cdab605bcfa2a053052d0948fc35fde2d2f41503 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 16 Dec 2021 11:18:38 -0800 Subject: [PATCH 7/7] Revert parameterless API version --- .../Sources/Codable/Callable+Codable.swift | 8 ++-- .../Tests/IntegrationTests.swift | 37 ------------------- 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift index 77832ed058f..9d5d1b16530 100644 --- a/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift +++ b/FirebaseFunctionsSwift/Sources/Codable/Callable+Codable.swift @@ -73,7 +73,7 @@ public struct Callable { /// /// - 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, + public func call(_ data: Request, completion: @escaping (Result) -> Void) { do { @@ -119,7 +119,7 @@ public struct Callable { /// - 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, + public func callAsFunction(_ data: Request, completion: @escaping (Result) -> Void) { call(data, completion: completion) @@ -146,7 +146,7 @@ public struct Callable { /// /// - Returns: The decoded `Response` value @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) - public func call(_ data: Request? = nil, + public func call(_ data: Request, encoder: FirebaseDataEncoder = FirebaseDataEncoder(), decoder: FirebaseDataDecoder = FirebaseDataDecoder()) async throws -> Response { @@ -175,7 +175,7 @@ public struct Callable { /// - 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 { + public func callAsFunction(_ data: Request) async throws -> Response { return try await call(data) } #endif diff --git a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift index a4a312e17f9..bf73e641207 100644 --- a/FirebaseFunctionsSwift/Tests/IntegrationTests.swift +++ b/FirebaseFunctionsSwift/Tests/IntegrationTests.swift @@ -290,43 +290,6 @@ class IntegrationTests: XCTestCase { } #endif - // No parameters to call should be the same as passing nil. - // If no parameters are required, then the non-typed API - // is more appropriate since it specifically avoids defining - // type. - func testParameterless() { - let expectation = expectation(description: #function) - let function = functions.httpsCallable( - "nullTest", - requestAs: Int?.self, - responseAs: Int?.self - ) - function.call { result in - do { - let data = try result.get() - XCTAssertEqual(data, nil) - } catch { - XCTAssert(false, "Failed to unwrap the function result: \(error)") - } - expectation.fulfill() - } - waitForExpectations(timeout: 5) - } - - #if compiler(>=5.5) && canImport(_Concurrency) - @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) - func testParameterlessAsync() async throws { - let function = functions.httpsCallable( - "nullTest", - requestAs: Int?.self, - responseAs: Int?.self - ) - - let data = try await function.call() - XCTAssertEqual(data, nil) - } - #endif - func testMissingResult() { let expectation = expectation(description: #function) let function = functions.httpsCallable(