Skip to content

Commit 0629b7e

Browse files
authored
Merge pull request #351 from mattpolzin/any-codable-small-improvements
Improvements to AnyCodable equality and ability to Encode any Encodable.
2 parents 3545fc2 + 00144b2 commit 0629b7e

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ Many OpenAPIKit types support [Specification Extensions](https://github.com/OAI/
288288

289289
You can get or set specification extensions via the `vendorExtensions` property on any object that supports this feature. The keys are `Strings` beginning with the aforementioned "x-" prefix and the values are `AnyCodable`. If you set an extension without using the "x-" prefix, the prefix will be added upon encoding.
290290

291+
#### AnyCodable
292+
OpenAPIKit uses the `AnyCodable` type for vendor extensions and constructing examples for JSON Schemas. OpenAPIKit's `AnyCodable` type is an adaptation of the Flight School library that can be found [here](https://github.com/Flight-School/AnyCodable).
293+
291294
`AnyCodable` can be constructed from literals or explicitly. The following are all valid.
292295

293296
```swift
@@ -300,6 +303,25 @@ document.vendorExtensions["x-specialProperty4"] = ["hello": "world"]
300303
document.vendorExtensions["x-specialProperty5"] = AnyCodable("hello world")
301304
```
302305

306+
It is important to note that `AnyCodable` wraps Swift types in a way that keeps track of the Swift type used to construct it as much as possible, but if you encode an `AnyCodable` and then decode that result, the decoded value may not always be the same as the pre-encoded value started out. This is because many Swift types will encode to "stringy" values and then decode as simply `String` values. There are two ways to cope with this:
307+
1. When adding stringy values to structures that will be passed to `AnyCodable`, you can explicitly turn them into `String`s. For example, you can use `URL(...).absoluteString` both to specify you want the absolute value of the URL encoded and also to turn it into a `String` up front.
308+
2. When comparing `AnyCodable` values that have passed through a full encode/decode cycle, you can compare the `description` of the two `AnyCodable` values. This stringy result is _more likely_ to compare equivalently.
309+
310+
Keep in mind, this issue only occurs when you are comparing value `a` and value `b` for equality given that `b` is `a` after being encoded and then subsequently decoded.
311+
312+
The other sure-fire way to handle this (if you need encode-decode equality, not just equivalence) is to make sure you run both values being compared through encoding. For example, you might use the following function which doesn't even care if the input is `AnyCodable` or not:
313+
```swift
314+
func encodedEqual<A: Codable, B: Codable>(_ a: A, _ b: B) throws -> Bool {
315+
let a = try JSONEncoder().encode(a)
316+
let b = try JSONEncoder().encode(b)
317+
return a == b
318+
}
319+
```
320+
For example, the result of the following is `true`:
321+
```swift
322+
try encodeEqual(URL(string: "https://website.com"), AnyCodable(URL(string: "https://website.com")))
323+
```
324+
303325
### Dereferencing & Resolving
304326
In addition to looking something up in the `Components` object, you can entirely derefererence many OpenAPIKit types. A dereferenced type has had all of its references looked up (and all of its properties' references, all the way down).
305327

Sources/OpenAPIKitCore/AnyCodable/AnyCodable.swift

+6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ extension AnyCodable: Encodable {
8787
try container.encode(array.map { AnyCodable($0) })
8888
case let dictionary as [String: Any?]:
8989
try container.encode(dictionary.mapValues { AnyCodable($0) })
90+
case let encodableValue as Encodable:
91+
try container.encode(encodableValue)
9092
default:
9193
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyCodable value cannot be encoded")
9294
throw EncodingError.invalidValue(value, context)
@@ -196,6 +198,8 @@ extension AnyCodable: Equatable {
196198
return lhs == rhs
197199
case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]):
198200
return lhs == rhs
201+
case let (lhs as [String: Any], rhs as [String: Any]):
202+
return lhs.mapValues(AnyCodable.init) == rhs.mapValues(AnyCodable.init)
199203
case let (lhs as [String], rhs as [String]):
200204
return lhs == rhs
201205
case let (lhs as [Int], rhs as [Int]):
@@ -206,6 +210,8 @@ extension AnyCodable: Equatable {
206210
return lhs == rhs
207211
case let (lhs as [AnyCodable], rhs as [AnyCodable]):
208212
return lhs == rhs
213+
case let (lhs as [Any], rhs as [Any]):
214+
return lhs.map(AnyCodable.init) == rhs.map(AnyCodable.init)
209215
default:
210216
return false
211217
}

Tests/AnyCodableTests/AnyCodableTests.swift

+75
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,85 @@ class AnyCodableTests: XCTestCase {
4141
XCTAssertNotEqual(AnyCodable(()), AnyCodable(true))
4242
}
4343

44+
func testEqualityFromJSON() throws {
45+
let json = """
46+
{
47+
"boolean": true,
48+
"integer": 1,
49+
"string": "string",
50+
"array": [1, 2, 3],
51+
"nested": {
52+
"a": "alpha",
53+
"b": "bravo",
54+
"c": "charlie"
55+
}
56+
}
57+
""".data(using: .utf8)!
58+
let decoder = JSONDecoder()
59+
let anyCodable0 = try decoder.decode(AnyCodable.self, from: json)
60+
let anyCodable1 = try decoder.decode(AnyCodable.self, from: json)
61+
XCTAssertEqual(anyCodable0, anyCodable1)
62+
}
63+
64+
struct CustomEncodable: Encodable {
65+
let value1: String
66+
67+
func encode(to encoder: Encoder) throws {
68+
var container = encoder.singleValueContainer()
69+
try container.encode("hi hi hi " + value1)
70+
}
71+
}
72+
73+
func test_encodable() throws {
74+
let value = CustomEncodable(value1: "hello")
75+
let anyCodable = AnyCodable(value)
76+
let thing = try JSONEncoder().encode(anyCodable)
77+
XCTAssertEqual(String(data: thing, encoding: .utf8)!, "\"hi hi hi hello\"")
78+
}
79+
4480
func testVoidDescription() {
4581
XCTAssertEqual(String(describing: AnyCodable(Void())), "nil")
4682
}
4783

84+
func test_encodedDecodedURL() throws {
85+
let value = URL(string: "https://www.google.com")
86+
let anyCodable = AnyCodable(value)
87+
88+
// URL's absoluteString compares as equal to the wrapped any codable description.
89+
XCTAssertEqual(value?.absoluteString, anyCodable.description)
90+
91+
let encodedValue = try JSONEncoder().encode(value)
92+
let encodedAnyCodable = try JSONEncoder().encode(anyCodable)
93+
// the URL and the wrapped any codable encode as equals.
94+
XCTAssertEqual(encodedValue, encodedAnyCodable)
95+
96+
let decodedFromValue = try JSONDecoder().decode(AnyCodable.self, from: encodedValue)
97+
// the URL decoded as any codable has the same description as the original any codable wrapper.
98+
XCTAssertEqual(anyCodable.description, decodedFromValue.description)
99+
100+
let decodedFromAnyCodable = try JSONDecoder().decode(AnyCodable.self, from: encodedAnyCodable)
101+
// the decoded any codable has the same description as the original any codable wrapper.
102+
XCTAssertEqual(anyCodable.description, decodedFromAnyCodable.description)
103+
104+
func roundTripEqual<A: Codable, B: Codable>(_ a: A, _ b: B) throws -> Bool {
105+
let a = try JSONDecoder().decode(AnyCodable.self,
106+
from: JSONEncoder().encode(a))
107+
let b = try JSONDecoder().decode(AnyCodable.self,
108+
from: JSONEncoder().encode(b))
109+
return a == b
110+
}
111+
// if you encode/decode both, the URL and its AnyCodable wrapper are equal.
112+
try XCTAssert(roundTripEqual(anyCodable, value))
113+
114+
func encodedEqual<A: Codable, B: Codable>(_ a: A, _ b: B) throws -> Bool {
115+
let a = try JSONEncoder().encode(a)
116+
let b = try JSONEncoder().encode(b)
117+
return a == b
118+
}
119+
// if you just compare the encoded data, the URL and its AnyCodable wrapper are equal.
120+
try XCTAssert(encodedEqual(anyCodable, value))
121+
}
122+
48123
func testJSONDecoding() throws {
49124
let json = """
50125
{

0 commit comments

Comments
 (0)