Skip to content

Fix OpenAPIValueContainer serialization of nested values #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Sources/OpenAPIRuntime/Base/OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
}

Expand All @@ -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(_:))
}

Expand Down Expand Up @@ -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))
}

Expand All @@ -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(_:))
}

Expand Down
196 changes: 196 additions & 0 deletions Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift
Original file line number Diff line number Diff line change
@@ -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])
}
}
13 changes: 13 additions & 0 deletions Tests/OpenAPIRuntimeTests/Test_Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ class Test_Runtime: XCTestCase {
var testStructPrettyData: Data {
Data(testStructPrettyString.utf8)
}

func _testPrettyEncoded<Value: Encodable>(_ 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<Value: Decodable>(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) {
Expand Down