Skip to content

Commit

Permalink
bug fixes for AnyCodable null-like types
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpolzin committed Jan 28, 2025
1 parent 2ee233c commit 15f8f34
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 5 deletions.
33 changes: 32 additions & 1 deletion Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ import Foundation
You can encode or decode mixed-type values in dictionaries
and other collections that require `Encodable` or `Decodable` conformance
by declaring their contained type to be `AnyCodable`.

Note that there are some caveats related to the fact that this type centers around
encoding/decoding values. For example, some technically distinct nil-like types
are all encoded as `nil` and compare equally under the `AnyCodable` type:
- `nil`
- `NSNull()`
- `Void()`
*/
public struct AnyCodable: @unchecked Sendable {
// IMPORTANT:
Expand All @@ -54,7 +61,11 @@ protocol _Optional {
}

extension Optional: _Optional {
var isNil: Bool { return self == nil }
var isNil: Bool { self == nil }
}

extension NSNull: _Optional {
var isNil: Bool { true }
}

extension AnyCodable: Encodable {
Expand Down Expand Up @@ -178,8 +189,28 @@ extension AnyCodable: Decodable {
}
}

func isNilEquivalent(value: AnyCodable) -> Bool {
let valueIsNil: Bool

if let optionalValue = value.value as? _Optional,
optionalValue.isNil {
valueIsNil = true
} else if let _ = value.value as? Void {
valueIsNil = true
} else {
valueIsNil = false
}

return valueIsNil
}

extension AnyCodable: Equatable {
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
// special case for nil
if isNilEquivalent(value: lhs) && isNilEquivalent(value: rhs) {
return true
}

switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
Expand Down
22 changes: 18 additions & 4 deletions Tests/AnyCodableTests/AnyCodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ class AnyCodableTests: XCTestCase {
}

func testEquality() throws {
XCTAssertEqual(AnyCodable(()), AnyCodable(()))
// nil, NSNull(), and Void() all encode as "null" and
// compare equally.
XCTAssertEqual(AnyCodable(nil), AnyCodable(nil))
XCTAssertEqual(AnyCodable(nil), AnyCodable(NSNull()))
XCTAssertEqual(AnyCodable(nil), AnyCodable(()))

XCTAssertEqual(AnyCodable(true), AnyCodable(true))
XCTAssertEqual(AnyCodable(2), AnyCodable(2))
XCTAssertEqual(AnyCodable(Int8(2)), AnyCodable(Int8(2)))
Expand Down Expand Up @@ -53,7 +58,8 @@ class AnyCodableTests: XCTestCase {
"a": "alpha",
"b": "bravo",
"c": "charlie"
}
},
"null": null
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
Expand Down Expand Up @@ -133,7 +139,8 @@ class AnyCodableTests: XCTestCase {
"a": "alpha",
"b": "bravo",
"c": "charlie"
}
},
"null": null
}
""".data(using: .utf8)!

Expand All @@ -146,6 +153,7 @@ class AnyCodableTests: XCTestCase {
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"], AnyCodable(nil))
}

func testJSONEncoding() throws {
Expand All @@ -159,6 +167,9 @@ class AnyCodableTests: XCTestCase {
"b": "bravo",
"c": "charlie",
]),
"null": nil,
"void": .init(Void()),
"nsnull": .init(NSNull())
]

let result = try testStringFromEncoding(of: dictionary)
Expand All @@ -179,7 +190,10 @@ class AnyCodableTests: XCTestCase {
"b" : "bravo",
"c" : "charlie"
},
"string" : "string"
"nsnull" : null,
"null" : null,
"string" : "string",
"void" : null
}
"""
)
Expand Down

0 comments on commit 15f8f34

Please sign in to comment.