diff --git a/.gitignore b/.gitignore index f6f5465e..c01c56a8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ DerivedData/ /Package.resolved .ci/ .docc-build/ +.swiftpm diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 2bb98f4c..a1be0397 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -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. @@ -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) @@ -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 diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 89a4a5a8..7049502e 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -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 @@ -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