From f2ba4c62d3df35653597298a9411e167c54b9b7c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 14 Jul 2023 11:42:41 +0200 Subject: [PATCH 1/5] Fix OpenAPIValueContainer serialization of nested values --- .../OpenAPIRuntime/Base/OpenAPIValue.swift | 8 +- .../Base/Test_OpenAPIValue.swift | 194 ++++++++++++++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 13 ++ 3 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index ab6a9f65..9902cea9 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 [Sendable?]: try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) - case let value as [String: OpenAPIValueContainer?]: + case let value as [String: 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 [Sendable]: + case let value as [Sendable?]: for item in value { hasher.combine(OpenAPIValueContainer(validatedValue: item)) } - case let value as [String: Sendable]: + case let value as [String: Sendable?]: for (key, itemValue) in value { hasher.combine(key) hasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift new file mode 100644 index 00000000..555f3217 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -0,0 +1,194 @@ +//===----------------------------------------------------------------------===// +// +// 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]]) + + _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello"]) + _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2]]) + } + + func testEncoding_container_success() throws { + let values: [Any?] = [ + nil, + "Hello", + [ + "key": "value", + "anotherKey": [ + 1, + "two", + ], + ], + 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? [Sendable?]) + XCTAssertEqual(array.count, 6) + XCTAssertNil(array[0]) + XCTAssertEqual(array[1] as? String, "Hello") + let dict = try XCTUnwrap(array[2] as? [String: Sendable?]) + XCTAssertEqual(dict.count, 2) + let nestedArray = try XCTUnwrap(dict["anotherKey"] as? [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? [Sendable?]) + XCTAssertEqual(boolArray.count, 1) + XCTAssertEqual(boolArray[0] as? Bool, true) + } + + func testEncoding_object_success() throws { + let values: [String: Any?] = [ + "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?] = [ + "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) { From a413710107e984afb75859e6d69294ee776c74a4 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 17 Jul 2023 11:41:06 +0200 Subject: [PATCH 2/5] Add missing existential anys --- Sources/OpenAPIRuntime/Base/OpenAPIValue.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 379dac95..c0d547c5 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 [Sendable?]: + case let value as [(any Sendable)?]: try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) - case let value as [String: Sendable?]: + case let value as [String: (any Sendable)?]: try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:))) default: throw EncodingError.invalidValue( From d6b2fb54ae8943ea036a7a55c880bddf9ac65122 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 17 Jul 2023 12:04:35 +0200 Subject: [PATCH 3/5] Adapt unit tests --- Sources/OpenAPIRuntime/Base/OpenAPIValue.swift | 4 ++-- Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index c0d547c5..62f82d87 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -52,7 +52,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter unvalidatedValue: A value of a JSON-compatible type, /// such as `String`, `[Any]`, and `[String: Any]`. /// - Throws: When the value is not supported. - public init(unvalidatedValue: (any Sendable)? = nil) throws { + public init(unvalidatedValue: Any? = nil) throws { try self.init(validatedValue: Self.tryCast(unvalidatedValue)) } @@ -62,7 +62,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter value: An untyped value. /// - Returns: A cast value if supported. /// - Throws: When the value is not supported. - static func tryCast(_ value: (any Sendable)?) throws -> (any Sendable)? { + static func tryCast(_ value: Any?) throws -> (any Sendable)? { guard let value = value else { return nil } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 555f3217..67b3c0d6 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -106,20 +106,20 @@ final class Test_OpenAPIValue: Test_Runtime { """# let container: OpenAPIValueContainer = try _getDecoded(json: json) let value = try XCTUnwrap(container.value) - let array = try XCTUnwrap(value as? [Sendable?]) + 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: Sendable?]) + let dict = try XCTUnwrap(array[2] as? [String: (any Sendable)?]) XCTAssertEqual(dict.count, 2) - let nestedArray = try XCTUnwrap(dict["anotherKey"] as? [Sendable?]) + 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? [Sendable?]) + let boolArray = try XCTUnwrap(array[5] as? [(any Sendable)?]) XCTAssertEqual(boolArray.count, 1) XCTAssertEqual(boolArray[0] as? Bool, true) } From 76808c05b057d01cfb97cdb9b2fcefea70e7debd Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 17 Jul 2023 12:43:48 +0200 Subject: [PATCH 4/5] Fix up all issues --- Sources/OpenAPIRuntime/Base/OpenAPIValue.swift | 12 ++++++------ .../Base/Test_OpenAPIValue.swift | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 62f82d87..040bf121 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -52,7 +52,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter unvalidatedValue: A value of a JSON-compatible type, /// such as `String`, `[Any]`, and `[String: Any]`. /// - Throws: When the value is not supported. - public init(unvalidatedValue: Any? = nil) throws { + public init(unvalidatedValue: (any Sendable)? = nil) throws { try self.init(validatedValue: Self.tryCast(unvalidatedValue)) } @@ -62,7 +62,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable { /// - Parameter value: An untyped value. /// - Returns: A cast value if supported. /// - Throws: When the value is not supported. - static func tryCast(_ value: Any?) throws -> (any Sendable)? { + static func tryCast(_ value: (any Sendable)?) throws -> (any Sendable)? { guard let value = value else { return nil } @@ -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 index 67b3c0d6..4c2647d6 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -26,14 +26,14 @@ final class Test_OpenAPIValue: Test_Runtime { _ = try OpenAPIValueContainer(unvalidatedValue: ["hello": "world"]) _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": "world"]) - _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": ["nested": "world", "nested2": 2]]) + _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": ["nested": "world", "nested2": 2] as [String: any Sendable]]) _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello"]) - _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2]]) + _ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2] as [any Sendable]]) } func testEncoding_container_success() throws { - let values: [Any?] = [ + let values: [(any Sendable)?] = [ nil, "Hello", [ @@ -41,8 +41,8 @@ final class Test_OpenAPIValue: Test_Runtime { "anotherKey": [ 1, "two", - ], - ], + ] as [any Sendable], + ] as [String: any Sendable], 1 as Int, 2.5 as Double, [true], @@ -125,7 +125,7 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncoding_object_success() throws { - let values: [String: Any?] = [ + let values: [String: (any Sendable)?] = [ "key": "value", "keyMore": [ true @@ -160,7 +160,7 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncoding_array_success() throws { - let values: [Any?] = [ + let values: [(any Sendable)?] = [ "one", ["two": 2], ] From 932a8fbda307d4b41b0bab1bb1c14016fd5b7a29 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 17 Jul 2023 12:47:15 +0200 Subject: [PATCH 5/5] Formatting fixes --- Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 4c2647d6..e61187dc 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -26,7 +26,9 @@ final class Test_OpenAPIValue: Test_Runtime { _ = try OpenAPIValueContainer(unvalidatedValue: ["hello": "world"]) _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": "world"]) - _ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": ["nested": "world", "nested2": 2] as [String: any Sendable]]) + _ = 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]])