Skip to content

Commit 71fcfa7

Browse files
authored
Improved encoding of NSNumber in OpenAPIValueContainer (#110)
Improved encoding of NSNumber in OpenAPIValueContainer ### Motivation When getting CoreFoundation/Foundation types, especially numbers, they automatically bridge to Swift types like Bool, Int, etc. That casting is pretty flexible, and allows e.g. casting a number into a boolean, which isn't desired when encoding into JSON, as `false` and `0` represent very different values. Previously, we relied on the automatic casting to know how to encode values, however that produced incorrect results in some cases. ### Modifications Add explicit handling of CF/NS types and try to encode using that new method before falling back to testing for native Swift types. This ensures that the original intention of the creator of the CF/NS types doesn't get lost in encoding. ### Result Correct encoding into JSON of types produced in the CF/NS world, like JSONSerialization. ### Test Plan Added unit tests. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (5.9.0) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #110
1 parent 39fa3ec commit 71fcfa7

File tree

3 files changed

+96
-6
lines changed

3 files changed

+96
-6
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ DerivedData/
99
/Package.resolved
1010
.ci/
1111
.docc-build/
12+
.swiftpm

Sources/OpenAPIRuntime/Base/OpenAPIValue.swift

+42
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import class Foundation.NSNull
1818
#else
1919
@preconcurrency import class Foundation.NSNull
2020
#endif
21+
import class Foundation.NSNumber
22+
import CoreFoundation
2123
#endif
2224

2325
/// A container for a value represented by JSON Schema.
@@ -139,6 +141,10 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
139141
try container.encodeNil()
140142
return
141143
}
144+
if let nsNumber = value as? NSNumber {
145+
try encode(nsNumber, to: &container)
146+
return
147+
}
142148
#endif
143149
switch value {
144150
case let value as Bool: try container.encode(value)
@@ -156,6 +162,42 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
156162
)
157163
}
158164
}
165+
/// Encodes the provided NSNumber based on its internal representation.
166+
/// - Parameters:
167+
/// - value: The NSNumber that boxes one of possibly many different types of values.
168+
/// - container: The container to encode the value in.
169+
/// - Throws: An error if the encoding process encounters issues or if the value is invalid.
170+
private func encode(_ value: NSNumber, to container: inout any SingleValueEncodingContainer) throws {
171+
if value === kCFBooleanTrue {
172+
try container.encode(true)
173+
} else if value === kCFBooleanFalse {
174+
try container.encode(false)
175+
} else {
176+
#if canImport(ObjectiveC)
177+
let nsNumber = value as CFNumber
178+
#else
179+
let nsNumber = unsafeBitCast(value, to: CFNumber.self)
180+
#endif
181+
let type = CFNumberGetType(nsNumber)
182+
switch type {
183+
case .sInt8Type, .charType: try container.encode(value.int8Value)
184+
case .sInt16Type, .shortType: try container.encode(value.int16Value)
185+
case .sInt32Type, .intType: try container.encode(value.int32Value)
186+
case .sInt64Type, .longLongType: try container.encode(value.int64Value)
187+
case .float32Type, .floatType: try container.encode(value.floatValue)
188+
case .float64Type, .doubleType, .cgFloatType: try container.encode(value.doubleValue)
189+
case .nsIntegerType, .longType, .cfIndexType: try container.encode(value.intValue)
190+
default:
191+
throw EncodingError.invalidValue(
192+
value,
193+
.init(
194+
codingPath: container.codingPath,
195+
debugDescription: "OpenAPIValueContainer cannot encode NSNumber of the underlying type: \(type)"
196+
)
197+
)
198+
}
199+
}
200+
}
159201

160202
// MARK: Equatable
161203

Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift

+53-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@
1313
//===----------------------------------------------------------------------===//
1414
import XCTest
1515
#if canImport(Foundation)
16-
#if canImport(Darwin)
17-
import class Foundation.NSNull
18-
#else
19-
@preconcurrency import class Foundation.NSNull
20-
#endif
16+
@preconcurrency import Foundation
17+
import CoreFoundation
2118
#endif
2219
@_spi(Generated) @testable import OpenAPIRuntime
2320

@@ -80,8 +77,58 @@ final class Test_OpenAPIValue: Test_Runtime {
8077
"""#
8178
try _testPrettyEncoded(container, expectedJSON: expectedString)
8279
}
83-
#endif
8480

81+
func testEncodingNSNumber() throws {
82+
func assertEncodedCF(
83+
_ value: CFNumber,
84+
as encodedValue: String,
85+
file: StaticString = #filePath,
86+
line: UInt = #line
87+
) throws {
88+
#if canImport(ObjectiveC)
89+
let nsNumber = value as NSNumber
90+
#else
91+
let nsNumber = unsafeBitCast(self, to: NSNumber.self)
92+
#endif
93+
try assertEncoded(nsNumber, as: encodedValue, file: file, line: line)
94+
}
95+
func assertEncoded(
96+
_ value: NSNumber,
97+
as encodedValue: String,
98+
file: StaticString = #filePath,
99+
line: UInt = #line
100+
) throws {
101+
let container = try OpenAPIValueContainer(unvalidatedValue: value)
102+
let encoder = JSONEncoder()
103+
encoder.outputFormatting = .sortedKeys
104+
let data = try encoder.encode(container)
105+
XCTAssertEqual(String(decoding: data, as: UTF8.self), encodedValue, file: file, line: line)
106+
}
107+
try assertEncoded(NSNumber(value: true as Bool), as: "true")
108+
try assertEncoded(NSNumber(value: false as Bool), as: "false")
109+
try assertEncoded(NSNumber(value: 24 as Int8), as: "24")
110+
try assertEncoded(NSNumber(value: 24 as Int16), as: "24")
111+
try assertEncoded(NSNumber(value: 24 as Int32), as: "24")
112+
try assertEncoded(NSNumber(value: 24 as Int64), as: "24")
113+
try assertEncoded(NSNumber(value: 24 as Int), as: "24")
114+
try assertEncoded(NSNumber(value: 24 as UInt8), as: "24")
115+
try assertEncoded(NSNumber(value: 24 as UInt16), as: "24")
116+
try assertEncoded(NSNumber(value: 24 as UInt32), as: "24")
117+
try assertEncoded(NSNumber(value: 24 as UInt64), as: "24")
118+
try assertEncoded(NSNumber(value: 24 as UInt), as: "24")
119+
#if canImport(ObjectiveC)
120+
try assertEncoded(NSNumber(value: 24 as NSInteger), as: "24")
121+
#endif
122+
try assertEncoded(NSNumber(value: 24 as CFIndex), as: "24")
123+
try assertEncoded(NSNumber(value: 24.1 as Float32), as: "24.1")
124+
try assertEncoded(NSNumber(value: 24.1 as Float64), as: "24.1")
125+
try assertEncoded(NSNumber(value: 24.1 as Float), as: "24.1")
126+
try assertEncoded(NSNumber(value: 24.1 as Double), as: "24.1")
127+
XCTAssertThrowsError(try assertEncodedCF(kCFNumberNaN, as: "-"))
128+
XCTAssertThrowsError(try assertEncodedCF(kCFNumberNegativeInfinity, as: "-"))
129+
XCTAssertThrowsError(try assertEncodedCF(kCFNumberPositiveInfinity, as: "-"))
130+
}
131+
#endif
85132
func testEncoding_container_failure() throws {
86133
struct Foobar: Equatable {}
87134
XCTAssertThrowsError(try OpenAPIValueContainer(unvalidatedValue: Foobar())) { error in

0 commit comments

Comments
 (0)