Skip to content

Commit

Permalink
Functions Serializer Updates
Browse files Browse the repository at this point in the history
* Updated `FUNSerializer`:
  * Nested the error type inside `FUNSerializer`
  * Extended `Error.unsupportedType` to include the type name (as resolved at runtime)
  * Refactored `decode(_:)`’s handling of dictionaries
  * Removed the explicit initializer
* Updated `SerializerTests`:
  * Introduced tests for encoding and decoding values of unsupported types
  * Moved `serializer` initialization to `setUp()`
  * Removed commented test (seems like it’s already implemented in Swift)
  • Loading branch information
yakovmanshin committed Jul 26, 2024
1 parent f18260f commit 67e9680
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 74 deletions.
55 changes: 23 additions & 32 deletions FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ private enum Constants {
static let dateType = "type.googleapis.com/google.protobuf.Timestamp"
}

enum SerializerError: Error {
// TODO: Add parameters class name and value
case unsupportedType // (className: String, value: AnyObject)
case unknownNumberType(charValue: String, number: NSNumber)
case invalidValueForType(value: String, requestedType: String)
extension FUNSerializer {
enum Error: Swift.Error {
case unsupportedType(typeName: String)
case unknownNumberType(charValue: String, number: NSNumber)
case invalidValueForType(value: String, requestedType: String)
}
}

class FUNSerializer: NSObject {
private let dateFormatter: DateFormatter

override init() {
dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
dateFormatter.timeZone = TimeZone(identifier: "UTC")
}
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = TimeZone(identifier: "UTC")
return formatter
}()

// MARK: - Internal APIs

Expand Down Expand Up @@ -67,7 +67,7 @@ class FUNSerializer: NSObject {
return encoded

} else {
throw SerializerError.unsupportedType
throw Error.unsupportedType(typeName: typeName(of: object))
}
}

Expand All @@ -86,21 +86,8 @@ class FUNSerializer: NSObject {
}

let decoded = NSMutableDictionary()
var decodeError: Error?
dict.enumerateKeysAndObjects { key, obj, stopPointer in
do {
let decodedItem = try self.decode(obj)
decoded[key] = decodedItem
} catch {
decodeError = error
stopPointer.pointee = true
return
}
}

// Throw the internal error that popped up, if it did.
if let decodeError {
throw decodeError
try dict.forEach { key, value in
decoded[key] = try decode(value)
}
return decoded
} else if let array = object as? NSArray {
Expand All @@ -116,11 +103,15 @@ class FUNSerializer: NSObject {
return object as AnyObject
}

throw SerializerError.unsupportedType
throw Error.unsupportedType(typeName: typeName(of: object))
}

// MARK: - Private Helpers

private func typeName(of value: Any) -> String {
String(describing: type(of: value))
}

private func encodeNumber(_ number: NSNumber) throws -> AnyObject {
// Recover the underlying type of the number, using the method described here:
// http://stackoverflow.com/questions/2518761/get-type-of-nsnumber
Expand Down Expand Up @@ -163,7 +154,7 @@ class FUNSerializer: NSObject {

default:
// All documented codes should be handled above, so this shouldn"t happen.
throw SerializerError.unknownNumberType(charValue: String(cType[0]), number: number)
throw Error.unknownNumberType(charValue: String(cType[0]), number: number)
}
}

Expand All @@ -172,7 +163,7 @@ class FUNSerializer: NSObject {
case Constants.longType:
let formatter = NumberFormatter()
guard let n = formatter.number(from: value) else {
throw SerializerError.invalidValueForType(value: value, requestedType: type)
throw Error.invalidValueForType(value: value, requestedType: type)
}
return n

Expand All @@ -182,7 +173,7 @@ class FUNSerializer: NSObject {
var endPtr: UnsafeMutablePointer<CChar>?
let returnValue = UInt64(strtoul(str, &endPtr, 10))
guard String(returnValue) == value else {
throw SerializerError.invalidValueForType(value: value, requestedType: type)
throw Error.invalidValueForType(value: value, requestedType: type)
}
return NSNumber(value: returnValue)

Expand Down
87 changes: 45 additions & 42 deletions FirebaseFunctions/Tests/Unit/SerializerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,67 +25,64 @@ import FirebaseCore
import XCTest

class SerializerTests: XCTestCase {
private var serializer: FUNSerializer!

override func setUp() {
super.setUp()
serializer = FUNSerializer()
}

func testEncodeNull() throws {
let serializer = FUNSerializer()
let null = NSNull()
XCTAssertEqual(try serializer.encode(null) as? NSNull, null)
}

func testDecodeNull() throws {
let serializer = FUNSerializer()
let null = NSNull()
XCTAssertEqual(try serializer.decode(null) as? NSNull, null)
}

func testEncodeInt32() throws {
let serializer = FUNSerializer()
let one = NSNumber(value: 1 as Int32)
XCTAssertEqual(one, try serializer.encode(one) as? NSNumber)
}

func testEncodeInt() throws {
let serializer = FUNSerializer()
let one = NSNumber(1)
let dict = try XCTUnwrap(serializer.encode(one) as? NSDictionary)
XCTAssertEqual("type.googleapis.com/google.protobuf.Int64Value", dict["@type"] as? String)
XCTAssertEqual("1", dict["value"] as? String)
}

func testDecodeInt32() throws {
let serializer = FUNSerializer()
let one = NSNumber(value: 1 as Int32)
XCTAssertEqual(one, try serializer.decode(one) as? NSNumber)
}

func testDecodeInt() throws {
let serializer = FUNSerializer()
let one = NSNumber(1)
XCTAssertEqual(one, try serializer.decode(one) as? NSNumber)
}

func testDecodeIntFromDictionary() throws {
let serializer = FUNSerializer()
let dictOne = ["@type": "type.googleapis.com/google.protobuf.Int64Value",
"value": "1"]
XCTAssertEqual(NSNumber(1), try serializer.decode(dictOne) as? NSNumber)
}

func testEncodeLong() throws {
let serializer = FUNSerializer()
let lowLong = NSNumber(-9_223_372_036_854_775_800)
let dict = try XCTUnwrap(serializer.encode(lowLong) as? NSDictionary)
XCTAssertEqual("type.googleapis.com/google.protobuf.Int64Value", dict["@type"] as? String)
XCTAssertEqual("-9223372036854775800", dict["value"] as? String)
}

func testDecodeLong() throws {
let serializer = FUNSerializer()
let lowLong = NSNumber(-9_223_372_036_854_775_800)
XCTAssertEqual(lowLong, try serializer.decode(lowLong) as? NSNumber)
}

func testDecodeLongFromDictionary() throws {
let serializer = FUNSerializer()
let dictLowLong = ["@type": "type.googleapis.com/google.protobuf.Int64Value",
"value": "-9223372036854775800"]
let decoded = try serializer.decode(dictLowLong) as? NSNumber
Expand All @@ -96,13 +93,12 @@ class SerializerTests: XCTestCase {
}

func testDecodeInvalidLong() throws {
let serializer = FUNSerializer()
let typeString = "type.googleapis.com/google.protobuf.Int64Value"
let badVal = "-9223372036854775800 and some other junk"
let dictLowLong = ["@type": typeString, "value": badVal]
do {
_ = try serializer.decode(dictLowLong) as? NSNumber
} catch let SerializerError.invalidValueForType(value, type) {
} catch let FUNSerializer.Error.invalidValueForType(value, type) {
XCTAssertEqual(value, badVal)
XCTAssertEqual(type, typeString)
return
Expand All @@ -111,7 +107,6 @@ class SerializerTests: XCTestCase {
}

func testEncodeUnsignedLong() throws {
let serializer = FUNSerializer()
let typeString = "type.googleapis.com/google.protobuf.UInt64Value"
let highULong = NSNumber(value: 18_446_744_073_709_551_607 as UInt64)
let expected = ["@type": typeString, "value": "18446744073709551607"]
Expand All @@ -120,13 +115,11 @@ class SerializerTests: XCTestCase {
}

func testDecodeUnsignedLong() throws {
let serializer = FUNSerializer()
let highULong = NSNumber(value: 18_446_744_073_709_551_607 as UInt64)
XCTAssertEqual(highULong, try serializer.decode(highULong) as? NSNumber)
}

func testDecodeUnsignedLongFromDictionary() throws {
let serializer = FUNSerializer()
let typeString = "type.googleapis.com/google.protobuf.UInt64Value"
let highULong = NSNumber(value: 18_446_744_073_709_551_607 as UInt64)
let coded = ["@type": typeString, "value": "18446744073709551607"]
Expand All @@ -138,13 +131,12 @@ class SerializerTests: XCTestCase {
}

func testDecodeUnsignedLongFromDictionaryOverflow() throws {
let serializer = FUNSerializer()
let typeString = "type.googleapis.com/google.protobuf.UInt64Value"
let tooHighVal = "18446744073709551616"
let coded = ["@type": typeString, "value": tooHighVal]
do {
_ = try serializer.decode(coded) as? NSNumber
} catch let SerializerError.invalidValueForType(value, type) {
} catch let FUNSerializer.Error.invalidValueForType(value, type) {
XCTAssertEqual(value, tooHighVal)
XCTAssertEqual(type, typeString)
return
Expand All @@ -153,47 +145,39 @@ class SerializerTests: XCTestCase {
}

func testEncodeDouble() throws {
let serializer = FUNSerializer()
let myDouble = NSNumber(value: 1.2 as Double)
XCTAssertEqual(myDouble, try serializer.encode(myDouble) as? NSNumber)
}

func testDecodeDouble() throws {
let serializer = FUNSerializer()
let myDouble = NSNumber(value: 1.2 as Double)
XCTAssertEqual(myDouble, try serializer.decode(myDouble) as? NSNumber)
}

func testEncodeBool() throws {
let serializer = FUNSerializer()
XCTAssertEqual(true, try serializer.encode(true) as? NSNumber)
}

func testDecodeBool() throws {
let serializer = FUNSerializer()
XCTAssertEqual(true, try serializer.decode(true) as? NSNumber)
}

func testEncodeString() throws {
let serializer = FUNSerializer()
XCTAssertEqual("hello", try serializer.encode("hello") as? String)
}

func testDecodeString() throws {
let serializer = FUNSerializer()
XCTAssertEqual("good-bye", try serializer.decode("good-bye") as? String)
}

// TODO: Should we add support for Array as well as NSArray?

func testEncodeSimpleArray() throws {
let serializer = FUNSerializer()
let input = [1 as Int32, 2 as Int32] as NSArray
XCTAssertEqual(input, try serializer.encode(input) as? NSArray)
}

func testEncodeArray() throws {
let serializer = FUNSerializer()
let input = [
1 as Int32,
"two",
Expand All @@ -204,7 +188,6 @@ class SerializerTests: XCTestCase {
}

func testDecodeArray() throws {
let serializer = FUNSerializer()
let input = [
1 as Int64,
"two",
Expand All @@ -227,44 +210,64 @@ class SerializerTests: XCTestCase {
"baz": [3, ["@type": "type.googleapis.com/google.protobuf.Int64Value",
"value": "9876543210"]] as [Any],
] as NSDictionary
let serializer = FUNSerializer()
XCTAssertEqual(expected, try serializer.encode(input) as? NSDictionary)
}

func testDecodeMap() {
let input = ["foo": 1, "bar": "hello", "baz": [3, 9_876_543_210]] as NSDictionary
let expected = ["foo": 1, "bar": "hello", "baz": [3, 9_876_543_210]] as NSDictionary
let serializer = FUNSerializer()
XCTAssertEqual(expected, try serializer.decode(input) as? NSDictionary)
}

func testEncodeUnknownType() {
let input = ["@type": "unknown", "value": "whatever"] as NSDictionary
let serializer = FUNSerializer()
XCTAssertEqual(input, try serializer.encode(input) as? NSDictionary)
}

func testDecodeUnknownType() {
let input = ["@type": "unknown", "value": "whatever"] as NSDictionary
let serializer = FUNSerializer()
XCTAssertEqual(input, try serializer.decode(input) as? NSDictionary)
}

func testDecodeUnknownTypeWithoutValue() {
let input = ["@type": "unknown"] as NSDictionary
let serializer = FUNSerializer()
XCTAssertEqual(input, try serializer.decode(input) as? NSDictionary)
}

// - (void)testDecodeUnknownTypeWithoutValue {
// NSDictionary *input = @{
// @"@type" : @"unknown",
// };
// FUNSerializer *serializer = [[FUNSerializer alloc] init];
// NSError *error = nil;
// XCTAssertEqualObjects(input, [serializer decode:input error:&error]);
// XCTAssertNil(error);
// }
//
// @end
func testEncodeUnsupportedType() {
let input = CustomObject()

do {
let _ = try serializer.encode(input)
XCTFail("Expected an error")
} catch {
guard case let .unsupportedType(typeName: typeName) = error as? FUNSerializer.Error
else {
return XCTFail("Unexpected error: \(error)")
}

XCTAssertEqual(typeName, "CustomObject")
}
}

func testDecodeUnsupportedType() {
let input = CustomObject()

do {
let _ = try serializer.decode(input)
XCTFail("Expected an error")
} catch {
guard case let .unsupportedType(typeName: typeName) = error as? FUNSerializer.Error
else {
return XCTFail("Unexpected error: \(error)")
}

XCTAssertEqual(typeName, "CustomObject")
}
}
}

/// Used to represent a type that cannot be encoded or decoded.
private struct CustomObject {
let id = 123
}

0 comments on commit 67e9680

Please sign in to comment.