Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Improve tests and fix encoding of NSNumber #84

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions Sources/AnyCodable/AnyEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ extension _AnyEncodable {
#if canImport(Foundation)
case is NSNull:
try container.encodeNil()
case let number as NSNumber:
try encode(nsnumber: number, into: &container)
#endif
case is Void:
try container.encodeNil()
Expand Down Expand Up @@ -87,8 +89,6 @@ extension _AnyEncodable {
case let string as String:
try container.encode(string)
#if canImport(Foundation)
case let number as NSNumber:
try encode(nsnumber: number, into: &container)
case let date as Date:
try container.encode(date)
case let url as URL:
Expand All @@ -108,28 +108,24 @@ extension _AnyEncodable {

#if canImport(Foundation)
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for applications where performance matters, this change should help make the code run faster.

case "B":
switch UInt32(nsnumber.objCType.pointee) {
case cpp_or_c99_bool_objc_encoding, char_objc_encoding, unsigned_char_objc_encoding:
try container.encode(nsnumber.boolValue)
case "c":
try container.encode(nsnumber.int8Value)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case is now mapped to a boolean

case "s":
case short_objc_encoding:
try container.encode(nsnumber.int16Value)
case "i", "l":
case int_objc_encoding, long_objc_encoding:
try container.encode(nsnumber.int32Value)
case "q":
case long_long_objc_encoding:
try container.encode(nsnumber.int64Value)
case "C":
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case is now mapped to a boolean

try container.encode(nsnumber.uint8Value)
case "S":
case unsigned_short_objc_encoding:
try container.encode(nsnumber.uint16Value)
case "I", "L":
case unsigned_int_objc_encoding, unsigned_long_objc_encoding:
try container.encode(nsnumber.uint32Value)
case "Q":
case unsigned_long_long_objc_encoding:
try container.encode(nsnumber.uint64Value)
case "f":
case float_objc_encoding:
try container.encode(nsnumber.floatValue)
case "d":
case double_objc_encoding:
try container.encode(nsnumber.doubleValue)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled")
Expand Down Expand Up @@ -289,3 +285,21 @@ extension AnyEncodable: Hashable {
}
}
}


#if canImport(Foundation)
// Types encodings: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
private let cpp_or_c99_bool_objc_encoding = "B".unicodeScalars.first?.value
private let char_objc_encoding = "c".unicodeScalars.first?.value
private let short_objc_encoding = "s".unicodeScalars.first?.value
private let int_objc_encoding = "i".unicodeScalars.first?.value
private let long_objc_encoding = "l".unicodeScalars.first?.value
private let long_long_objc_encoding = "q".unicodeScalars.first?.value
private let unsigned_char_objc_encoding = "C".unicodeScalars.first?.value
private let unsigned_short_objc_encoding = "S".unicodeScalars.first?.value
private let unsigned_int_objc_encoding = "I".unicodeScalars.first?.value
private let unsigned_long_objc_encoding = "L".unicodeScalars.first?.value
private let unsigned_long_long_objc_encoding = "Q".unicodeScalars.first?.value
private let float_objc_encoding = "f".unicodeScalars.first?.value
private let double_objc_encoding = "d".unicodeScalars.first?.value
#endif
21 changes: 9 additions & 12 deletions Tests/AnyCodableTests/AnyCodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class AnyCodableTests: XCTestCase {
let decoder = JSONDecoder()
let dictionary = try decoder.decode([String: AnyCodable].self, from: json)

XCTAssertEqual(dictionary["boolean"]?.value as! Bool, true)
XCTAssertEqual(dictionary["integer"]?.value as! Int, 42)
XCTAssertEqual(dictionary["double"]?.value as! Double, 3.141592653589793, accuracy: 0.001)
XCTAssertEqual(dictionary["string"]?.value as! String, "string")
XCTAssertEqual(dictionary["array"]?.value as! [Int], [1, 2, 3])
XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"])
XCTAssertEqual(dictionary["null"]?.value as! NSNull, NSNull())
XCTAssertEqual(dictionary["boolean"]?.value as? Bool, true)
XCTAssertEqual(dictionary["integer"]?.value as? Int, 42)
XCTAssertEqual(try XCTUnwrap(dictionary["double"]?.value as? Double), 3.141592653589793, accuracy: 0.001)
XCTAssertEqual(dictionary["string"]?.value as? String, "string")
XCTAssertEqual(dictionary["array"]?.value as? [Int], [1, 2, 3])
XCTAssertEqual(dictionary["nested"]?.value as? [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"])
XCTAssertEqual(dictionary["null"]?.value as? NSNull, NSNull())
}

func testJSONDecodingEquatable() throws {
Expand Down Expand Up @@ -102,7 +102,6 @@ class AnyCodableTests: XCTestCase {
let encoder = JSONEncoder()

let json = try encoder.encode(dictionary)
let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary

let expected = """
{
Expand All @@ -125,9 +124,7 @@ class AnyCodableTests: XCTestCase {
},
"null": null
}
""".data(using: .utf8)!
let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary

XCTAssertEqual(encodedJSONObject, expectedJSONObject)
"""
try XCTAssertJsonAreIdentical(json, expected)
}
}
18 changes: 9 additions & 9 deletions Tests/AnyCodableTests/AnyDecodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

class AnyDecodableTests: XCTestCase {
func testJSONDecoding() throws {
let json = """
let json = try XCTUnwrap("""
{
"boolean": true,
"integer": 42,
Expand All @@ -17,17 +17,17 @@ class AnyDecodableTests: XCTestCase {
},
"null": null
}
""".data(using: .utf8)!
""".data(using: .utf8))

let decoder = JSONDecoder()
let dictionary = try decoder.decode([String: AnyDecodable].self, from: json)

XCTAssertEqual(dictionary["boolean"]?.value as! Bool, true)
XCTAssertEqual(dictionary["integer"]?.value as! Int, 42)
XCTAssertEqual(dictionary["double"]?.value as! Double, 3.141592653589793, accuracy: 0.001)
XCTAssertEqual(dictionary["string"]?.value as! String, "string")
XCTAssertEqual(dictionary["array"]?.value as! [Int], [1, 2, 3])
XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"])
XCTAssertEqual(dictionary["null"]?.value as! NSNull, NSNull())
XCTAssertEqual(dictionary["boolean"]?.value as? Bool, true)
XCTAssertEqual(dictionary["integer"]?.value as? Int, 42)
XCTAssertEqual(try XCTUnwrap(dictionary["double"]?.value as? Double), 3.141592653589793, accuracy: 0.001)
XCTAssertEqual(dictionary["string"]?.value as? String, "string")
XCTAssertEqual(dictionary["array"]?.value as? [Int], [1, 2, 3])
XCTAssertEqual(dictionary["nested"]?.value as? [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"])
XCTAssertEqual(dictionary["null"]?.value as? NSNull, NSNull())
}
}
83 changes: 43 additions & 40 deletions Tests/AnyCodableTests/AnyEncodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ class AnyEncodableTests: XCTestCase {
func testJSONEncoding() throws {

let someEncodable = AnyEncodable(SomeEncodable(string: "String", int: 100, bool: true, hasUnderscore: "another string"))

let nsNumber = AnyEncodable(1 as NSNumber)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added NSNumber here to ensure there is no future regressions


let dictionary: [String: AnyEncodable] = [
"boolean": true,
"integer": 42,
"nsNumber": nsNumber,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
Expand All @@ -35,11 +37,8 @@ class AnyEncodableTests: XCTestCase {
"someCodable": someEncodable,
"null": nil
]
let json = try JSONEncoder().encode(dictionary)

let encoder = JSONEncoder()

let json = try encoder.encode(dictionary)
let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary

let expected = """
{
Expand All @@ -53,6 +52,7 @@ class AnyEncodableTests: XCTestCase {
"b": "bravo",
"c": "charlie"
},
"nsNumber": 1,
"someCodable": {
"string":"String",
"int":100,
Expand All @@ -61,10 +61,8 @@ class AnyEncodableTests: XCTestCase {
},
"null": null
}
""".data(using: .utf8)!
let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary

XCTAssertEqual(encodedJSONObject, expectedJSONObject)
"""
try XCTAssertJsonAreIdentical(json, expected)
}

func testEncodeNSNumber() throws {
Expand All @@ -83,10 +81,7 @@ class AnyEncodableTests: XCTestCase {
"double": 3.141592653589793,
]

let encoder = JSONEncoder()

let json = try encoder.encode(AnyEncodable(dictionary))
let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary
let json = try JSONEncoder().encode(AnyEncodable(dictionary))

let expected = """
{
Expand All @@ -103,25 +98,8 @@ class AnyEncodableTests: XCTestCase {
"ulonglong": 18446744073709615,
"double": 3.141592653589793,
}
""".data(using: .utf8)!
let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary

XCTAssertEqual(encodedJSONObject, expectedJSONObject)
XCTAssert(encodedJSONObject["boolean"] is Bool)

XCTAssert(encodedJSONObject["char"] is Int8)
XCTAssert(encodedJSONObject["int"] is Int16)
XCTAssert(encodedJSONObject["short"] is Int32)
XCTAssert(encodedJSONObject["long"] is Int32)
XCTAssert(encodedJSONObject["longlong"] is Int64)

XCTAssert(encodedJSONObject["uchar"] is UInt8)
XCTAssert(encodedJSONObject["uint"] is UInt16)
XCTAssert(encodedJSONObject["ushort"] is UInt32)
XCTAssert(encodedJSONObject["ulong"] is UInt32)
XCTAssert(encodedJSONObject["ulonglong"] is UInt64)

XCTAssert(encodedJSONObject["double"] is Double)
Comment on lines -108 to -124
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this part that is not testing decoding

"""
try XCTAssertJsonAreIdentical(json, expected)
}

func testStringInterpolationEncoding() throws {
Expand All @@ -132,11 +110,7 @@ class AnyEncodableTests: XCTestCase {
"string": "\("string")",
"array": "\([1, 2, 3])",
]

let encoder = JSONEncoder()

let json = try encoder.encode(dictionary)
let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary
let json = try JSONEncoder().encode(dictionary)

let expected = """
{
Expand All @@ -146,9 +120,38 @@ class AnyEncodableTests: XCTestCase {
"string": "string",
"array": "[1, 2, 3]",
}
""".data(using: .utf8)!
let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary
"""

XCTAssertEqual(encodedJSONObject, expectedJSONObject)
try XCTAssertJsonAreIdentical(json, expected)
}
}



func XCTAssertJsonAreIdentical(_ expression1: String, _ expression2: String, options: JSONSerialization.WritingOptions? = nil) throws {
Copy link
Author

@gsabran gsabran Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper functions to test that two serialized JSON (either String or Data) are equivalent (ie ignoring keys ordering and spaces).

let data = try XCTUnwrap(expression1.data(using: .utf8))
try XCTAssertJsonAreIdentical(data, expression2, options: options)
}

func XCTAssertJsonAreIdentical(_ expression1: String, _ expression2: Data, options: JSONSerialization.WritingOptions? = nil) throws {
let data = try XCTUnwrap(expression1.data(using: .utf8))
try XCTAssertJsonAreIdentical(data, expression2, options: options)
}

func XCTAssertJsonAreIdentical(_ expression1: Data, _ expression2: String, options: JSONSerialization.WritingOptions? = nil) throws {
let data = try XCTUnwrap(expression2.data(using: .utf8))
try XCTAssertJsonAreIdentical(expression1, data, options: options)
}

func XCTAssertJsonAreIdentical(_ expression1: Data, _ expression2: Data, options: JSONSerialization.WritingOptions? = nil) throws {
var defaultOptions: JSONSerialization.WritingOptions = []
if #available(iOS 11.0, *) {
defaultOptions = [.sortedKeys, .prettyPrinted]
} else {
defaultOptions = [.prettyPrinted]
}
XCTAssertEqual(
String(data: try JSONSerialization.data(withJSONObject: try JSONSerialization.jsonObject(with: expression1), options: options ?? defaultOptions), encoding: .utf8),
String(data: try JSONSerialization.data(withJSONObject: try JSONSerialization.jsonObject(with: expression2), options: options ?? defaultOptions), encoding: .utf8)
)
}