diff --git a/Sources/AnyCodable/AnyEncodable.swift b/Sources/AnyCodable/AnyEncodable.swift index d5530e5..21d8b6d 100644 --- a/Sources/AnyCodable/AnyEncodable.swift +++ b/Sources/AnyCodable/AnyEncodable.swift @@ -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() @@ -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: @@ -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))) { - 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) - 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": - 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") @@ -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 diff --git a/Tests/AnyCodableTests/AnyCodableTests.swift b/Tests/AnyCodableTests/AnyCodableTests.swift index 5e7770d..6540870 100644 --- a/Tests/AnyCodableTests/AnyCodableTests.swift +++ b/Tests/AnyCodableTests/AnyCodableTests.swift @@ -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 { @@ -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 = """ { @@ -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) } } diff --git a/Tests/AnyCodableTests/AnyDecodableTests.swift b/Tests/AnyCodableTests/AnyDecodableTests.swift index 147dc39..4193010 100644 --- a/Tests/AnyCodableTests/AnyDecodableTests.swift +++ b/Tests/AnyCodableTests/AnyDecodableTests.swift @@ -3,7 +3,7 @@ import XCTest class AnyDecodableTests: XCTestCase { func testJSONDecoding() throws { - let json = """ + let json = try XCTUnwrap(""" { "boolean": true, "integer": 42, @@ -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()) } } diff --git a/Tests/AnyCodableTests/AnyEncodableTests.swift b/Tests/AnyCodableTests/AnyEncodableTests.swift index bd81b70..975bea0 100644 --- a/Tests/AnyCodableTests/AnyEncodableTests.swift +++ b/Tests/AnyCodableTests/AnyEncodableTests.swift @@ -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) + let dictionary: [String: AnyEncodable] = [ "boolean": true, "integer": 42, + "nsNumber": nsNumber, "double": 3.141592653589793, "string": "string", "array": [1, 2, 3], @@ -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 = """ { @@ -53,6 +52,7 @@ class AnyEncodableTests: XCTestCase { "b": "bravo", "c": "charlie" }, + "nsNumber": 1, "someCodable": { "string":"String", "int":100, @@ -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 { @@ -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 = """ { @@ -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) + """ + try XCTAssertJsonAreIdentical(json, expected) } func testStringInterpolationEncoding() throws { @@ -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 = """ { @@ -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 { + 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) + ) +}