diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 4f274274..040bf121 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -139,9 +139,9 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { try container.encode(value) case let value as String: try container.encode(value) - case let value as [OpenAPIValueContainer?]: + case let value as [(any Sendable)?]: try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) - case let value as [String: OpenAPIValueContainer?]: + case let value as [String: (any Sendable)?]: try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:))) default: throw EncodingError.invalidValue( @@ -211,11 +211,11 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { hasher.combine(value) case let value as String: hasher.combine(value) - case let value as [any Sendable]: + case let value as [(any Sendable)?]: for item in value { hasher.combine(OpenAPIValueContainer(validatedValue: item)) } - case let value as [String: any Sendable]: + case let value as [String: (any Sendable)?]: for (key, itemValue) in value { hasher.combine(key) hasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) @@ -301,7 +301,7 @@ public struct OpenAPIObjectContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter unvalidatedValue: A dictionary with values of /// JSON-compatible types. /// - Throws: When the value is not supported. - public init(unvalidatedValue: [String: Any?]) throws { + public init(unvalidatedValue: [String: (any Sendable)?]) throws { try self.init(validatedValue: Self.tryCast(unvalidatedValue)) } @@ -311,7 +311,7 @@ public struct OpenAPIObjectContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter value: A dictionary with untyped values. /// - Returns: A cast dictionary if values are supported. /// - Throws: If an unsupported value is found. - static func tryCast(_ value: [String: Any?]) throws -> [String: (any Sendable)?] { + static func tryCast(_ value: [String: (any Sendable)?]) throws -> [String: (any Sendable)?] { return try value.mapValues(OpenAPIValueContainer.tryCast(_:)) } @@ -405,7 +405,7 @@ public struct OpenAPIArrayContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter unvalidatedValue: An array with values of JSON-compatible /// types. /// - Throws: When the value is not supported. - public init(unvalidatedValue: [Any?]) throws { + public init(unvalidatedValue: [(any Sendable)?]) throws { try self.init(validatedValue: Self.tryCast(unvalidatedValue)) } @@ -414,7 +414,7 @@ public struct OpenAPIArrayContainer: Codable, Equatable, Hashable, Sendable { /// Returns the specified value cast to an array of supported values. /// - Parameter value: An array with untyped values. /// - Returns: A cast value if values are supported, nil otherwise. - static func tryCast(_ value: [Any?]) throws -> [(any Sendable)?] { + static func tryCast(_ value: [(any Sendable)?]) throws -> [(any Sendable)?] { return try value.map(OpenAPIValueContainer.tryCast(_:)) } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift new file mode 100644 index 00000000..e61187dc --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) import OpenAPIRuntime + +final class Test_OpenAPIValue: Test_Runtime { + + func testValidationOnCreation() throws { + _ = OpenAPIValueContainer("hello") + _ = OpenAPIValueContainer(true) + _ = OpenAPIValueContainer(1) + _ = OpenAPIValueContainer(4.5) + + _ = try OpenAPIValueContainer(unvalidatedValue: ["hello"]) + _ = try OpenAPIValueContainer(unvalidatedValue: ["hello": "world"]) + + _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": "world"]) + _ = try OpenAPIObjectContainer(unvalidatedValue: [ + "hello": ["nested": "world", "nested2": 2] as [String: any Sendable] + ]) + + _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello"]) + _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2] as [any Sendable]]) + } + + func testEncoding_container_success() throws { + let values: [(any Sendable)?] = [ + nil, + "Hello", + [ + "key": "value", + "anotherKey": [ + 1, + "two", + ] as [any Sendable], + ] as [String: any Sendable], + 1 as Int, + 2.5 as Double, + [true], + ] + let container = try OpenAPIValueContainer(unvalidatedValue: values) + let expectedString = #""" + [ + null, + "Hello", + { + "anotherKey" : [ + 1, + "two" + ], + "key" : "value" + }, + 1, + 2.5, + [ + true + ] + ] + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testEncoding_container_failure() throws { + struct Foobar: Equatable {} + XCTAssertThrowsError(try OpenAPIValueContainer(unvalidatedValue: Foobar())) { error in + let err = try! XCTUnwrap(error as? EncodingError) + guard case let .invalidValue(value, context) = err else { + XCTFail("Unexpected error") + return + } + let typedValue = try! XCTUnwrap(value as? Foobar) + XCTAssertEqual(typedValue, Foobar()) + XCTAssert(context.codingPath.isEmpty) + XCTAssertNil(context.underlyingError) + XCTAssertEqual(context.debugDescription, "Type 'Foobar' is not a supported OpenAPI value.") + } + } + + func testDecoding_container_success() throws { + let json = #""" + [ + null, + "Hello", + { + "anotherKey" : [ + 1, + "two" + ], + "key" : "value" + }, + 1, + 2.5, + [ + true + ] + ] + """# + let container: OpenAPIValueContainer = try _getDecoded(json: json) + let value = try XCTUnwrap(container.value) + let array = try XCTUnwrap(value as? [(any Sendable)?]) + XCTAssertEqual(array.count, 6) + XCTAssertNil(array[0]) + XCTAssertEqual(array[1] as? String, "Hello") + let dict = try XCTUnwrap(array[2] as? [String: (any Sendable)?]) + XCTAssertEqual(dict.count, 2) + let nestedArray = try XCTUnwrap(dict["anotherKey"] as? [(any Sendable)?]) + XCTAssertEqual(nestedArray.count, 2) + XCTAssertEqual(nestedArray[0] as? Int, 1) + XCTAssertEqual(nestedArray[1] as? String, "two") + XCTAssertEqual(dict["key"] as? String, "value") + XCTAssertEqual(array[3] as? Int, 1) + XCTAssertEqual(array[4] as? Double, 2.5) + let boolArray = try XCTUnwrap(array[5] as? [(any Sendable)?]) + XCTAssertEqual(boolArray.count, 1) + XCTAssertEqual(boolArray[0] as? Bool, true) + } + + func testEncoding_object_success() throws { + let values: [String: (any Sendable)?] = [ + "key": "value", + "keyMore": [ + true + ], + ] + let container = try OpenAPIObjectContainer(unvalidatedValue: values) + let expectedString = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testDecoding_object_success() throws { + let json = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + let container: OpenAPIObjectContainer = try _getDecoded(json: json) + let value = container.value + XCTAssertEqual(value.count, 2) + XCTAssertEqual(value["key"] as? String, "value") + XCTAssertEqual(value["keyMore"] as? [Bool], [true]) + } + + func testEncoding_array_success() throws { + let values: [(any Sendable)?] = [ + "one", + ["two": 2], + ] + let container = try OpenAPIArrayContainer(unvalidatedValue: values) + let expectedString = #""" + [ + "one", + { + "two" : 2 + } + ] + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testDecoding_array_success() throws { + let json = #""" + [ + "one", + { + "two" : 2 + } + ] + """# + let container: OpenAPIArrayContainer = try _getDecoded(json: json) + let value = container.value + XCTAssertEqual(value.count, 2) + XCTAssertEqual(value[0] as? String, "one") + XCTAssertEqual(value[1] as? [String: Int], ["two": 2]) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 1f238583..6965ad51 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -97,6 +97,19 @@ class Test_Runtime: XCTestCase { var testStructPrettyData: Data { Data(testStructPrettyString.utf8) } + + func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(value) + XCTAssertEqual(String(data: data, encoding: .utf8)!, expectedJSON) + } + + func _getDecoded(json: String) throws -> Value { + let inputData = json.data(using: .utf8)! + let decoder = JSONDecoder() + return try decoder.decode(Value.self, from: inputData) + } } public func XCTAssertEqualURLString(_ lhs: URL?, _ rhs: String, file: StaticString = #file, line: UInt = #line) {