Skip to content

Commit

Permalink
Improved encoding of NSNumber in OpenAPIValueContainer (#110)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
czechboy0 authored Jul 19, 2024
1 parent 39fa3ec commit 71fcfa7
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ DerivedData/
/Package.resolved
.ci/
.docc-build/
.swiftpm
42 changes: 42 additions & 0 deletions Sources/OpenAPIRuntime/Base/OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import class Foundation.NSNull
#else
@preconcurrency import class Foundation.NSNull
#endif
import class Foundation.NSNumber
import CoreFoundation
#endif

/// A container for a value represented by JSON Schema.
Expand Down Expand Up @@ -139,6 +141,10 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
try container.encodeNil()
return
}
if let nsNumber = value as? NSNumber {
try encode(nsNumber, to: &container)
return
}
#endif
switch value {
case let value as Bool: try container.encode(value)
Expand All @@ -156,6 +162,42 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
)
}
}
/// Encodes the provided NSNumber based on its internal representation.
/// - Parameters:
/// - value: The NSNumber that boxes one of possibly many different types of values.
/// - container: The container to encode the value in.
/// - Throws: An error if the encoding process encounters issues or if the value is invalid.
private func encode(_ value: NSNumber, to container: inout any SingleValueEncodingContainer) throws {
if value === kCFBooleanTrue {
try container.encode(true)
} else if value === kCFBooleanFalse {
try container.encode(false)
} else {
#if canImport(ObjectiveC)
let nsNumber = value as CFNumber
#else
let nsNumber = unsafeBitCast(value, to: CFNumber.self)
#endif
let type = CFNumberGetType(nsNumber)
switch type {
case .sInt8Type, .charType: try container.encode(value.int8Value)
case .sInt16Type, .shortType: try container.encode(value.int16Value)
case .sInt32Type, .intType: try container.encode(value.int32Value)
case .sInt64Type, .longLongType: try container.encode(value.int64Value)
case .float32Type, .floatType: try container.encode(value.floatValue)
case .float64Type, .doubleType, .cgFloatType: try container.encode(value.doubleValue)
case .nsIntegerType, .longType, .cfIndexType: try container.encode(value.intValue)
default:
throw EncodingError.invalidValue(
value,
.init(
codingPath: container.codingPath,
debugDescription: "OpenAPIValueContainer cannot encode NSNumber of the underlying type: \(type)"
)
)
}
}
}

// MARK: Equatable

Expand Down
59 changes: 53 additions & 6 deletions Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@
//===----------------------------------------------------------------------===//
import XCTest
#if canImport(Foundation)
#if canImport(Darwin)
import class Foundation.NSNull
#else
@preconcurrency import class Foundation.NSNull
#endif
@preconcurrency import Foundation
import CoreFoundation
#endif
@_spi(Generated) @testable import OpenAPIRuntime

Expand Down Expand Up @@ -80,8 +77,58 @@ final class Test_OpenAPIValue: Test_Runtime {
"""#
try _testPrettyEncoded(container, expectedJSON: expectedString)
}
#endif

func testEncodingNSNumber() throws {
func assertEncodedCF(
_ value: CFNumber,
as encodedValue: String,
file: StaticString = #filePath,
line: UInt = #line
) throws {
#if canImport(ObjectiveC)
let nsNumber = value as NSNumber
#else
let nsNumber = unsafeBitCast(self, to: NSNumber.self)
#endif
try assertEncoded(nsNumber, as: encodedValue, file: file, line: line)
}
func assertEncoded(
_ value: NSNumber,
as encodedValue: String,
file: StaticString = #filePath,
line: UInt = #line
) throws {
let container = try OpenAPIValueContainer(unvalidatedValue: value)
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(container)
XCTAssertEqual(String(decoding: data, as: UTF8.self), encodedValue, file: file, line: line)
}
try assertEncoded(NSNumber(value: true as Bool), as: "true")
try assertEncoded(NSNumber(value: false as Bool), as: "false")
try assertEncoded(NSNumber(value: 24 as Int8), as: "24")
try assertEncoded(NSNumber(value: 24 as Int16), as: "24")
try assertEncoded(NSNumber(value: 24 as Int32), as: "24")
try assertEncoded(NSNumber(value: 24 as Int64), as: "24")
try assertEncoded(NSNumber(value: 24 as Int), as: "24")
try assertEncoded(NSNumber(value: 24 as UInt8), as: "24")
try assertEncoded(NSNumber(value: 24 as UInt16), as: "24")
try assertEncoded(NSNumber(value: 24 as UInt32), as: "24")
try assertEncoded(NSNumber(value: 24 as UInt64), as: "24")
try assertEncoded(NSNumber(value: 24 as UInt), as: "24")
#if canImport(ObjectiveC)
try assertEncoded(NSNumber(value: 24 as NSInteger), as: "24")
#endif
try assertEncoded(NSNumber(value: 24 as CFIndex), as: "24")
try assertEncoded(NSNumber(value: 24.1 as Float32), as: "24.1")
try assertEncoded(NSNumber(value: 24.1 as Float64), as: "24.1")
try assertEncoded(NSNumber(value: 24.1 as Float), as: "24.1")
try assertEncoded(NSNumber(value: 24.1 as Double), as: "24.1")
XCTAssertThrowsError(try assertEncodedCF(kCFNumberNaN, as: "-"))
XCTAssertThrowsError(try assertEncodedCF(kCFNumberNegativeInfinity, as: "-"))
XCTAssertThrowsError(try assertEncodedCF(kCFNumberPositiveInfinity, as: "-"))
}
#endif
func testEncoding_container_failure() throws {
struct Foobar: Equatable {}
XCTAssertThrowsError(try OpenAPIValueContainer(unvalidatedValue: Foobar())) { error in
Expand Down

0 comments on commit 71fcfa7

Please sign in to comment.