Skip to content

Commit

Permalink
[Runtime] Fix nested coding (#50)
Browse files Browse the repository at this point in the history
[Runtime] Fix nested coding

### Motivation

Fixes apple/swift-openapi-generator#263.

### Modifications

Makes URIEncoder/URIDecoder able to handle custom Codable types in a single value container.

For more details check out the associated generator [PR](apple/swift-openapi-generator#271).

### Result

More Codable types can be handled.

### Test Plan

Updated unit tests.


Reviewed by: glbrntt

Builds:
     ✔︎ pull request validation (5.8) - Build finished. 
     ✔︎ pull request validation (5.9) - 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. 

#50
  • Loading branch information
czechboy0 authored Sep 15, 2023
1 parent 9b003cc commit f4f5963
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 29 deletions.
33 changes: 33 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ extension Decoder {
return .init(uniqueKeysWithValues: keyValuePairs)
}

/// Returns the decoded value by using a single value container.
/// - Parameter type: The type to decode.
/// - Returns: The decoded value.
public func decodeFromSingleValueContainer<T: Decodable>(
_ type: T.Type = T.self
) throws -> T {
let container = try singleValueContainer()
return try container.decode(T.self)
}

// MARK: - Private

/// Returns the keys in the given decoder that are not present
Expand Down Expand Up @@ -146,6 +156,29 @@ extension Encoder {
try container.encode(value, forKey: .init(key))
}
}

/// Encodes the value into the encoder using a single value container.
/// - Parameter value: The value to encode.
public func encodeToSingleValueContainer<T: Encodable>(
_ value: T
) throws {
var container = singleValueContainer()
try container.encode(value)
}

/// Encodes the first non-nil value from the provided array into
/// the encoder using a single value container.
/// - Parameter values: An array of optional values.
public func encodeFirstNonNilValueToSingleValueContainer(
_ values: [(any Encodable)?]
) throws {
for value in values {
if let value {
try encodeToSingleValueContainer(value)
return
}
}
}
}

/// A freeform String coding key for decoding undocumented values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,27 @@ import Foundation
/// A single value container used by `URIValueFromNodeDecoder`.
struct URISingleValueDecodingContainer {

/// The coder used to serialize Date values.
let dateTranscoder: any DateTranscoder

/// The coding path of the container.
let codingPath: [any CodingKey]

/// The underlying value.
let value: URIParsedValue
/// The associated decoder.
let decoder: URIValueFromNodeDecoder
}

extension URISingleValueDecodingContainer {

/// The underlying value as a single value.
var value: URIParsedValue {
get throws {
try decoder.currentElementAsSingleValue()
}
}

/// Returns the value found in the underlying node converted to
/// the provided type.
/// - Returns: The converted value found.
/// - Throws: An error if the conversion failed.
private func _decodeBinaryFloatingPoint<T: BinaryFloatingPoint>(
_: T.Type = T.self
) throws -> T {
guard let double = Double(value) else {
guard let double = try Double(value) else {
throw DecodingError.typeMismatch(
T.self,
.init(
Expand All @@ -55,7 +56,7 @@ extension URISingleValueDecodingContainer {
private func _decodeFixedWidthInteger<T: FixedWidthInteger>(
_: T.Type = T.self
) throws -> T {
guard let parsedValue = T(value) else {
guard let parsedValue = try T(value) else {
throw DecodingError.typeMismatch(
T.self,
.init(
Expand All @@ -74,7 +75,7 @@ extension URISingleValueDecodingContainer {
private func _decodeLosslessStringConvertible<T: LosslessStringConvertible>(
_: T.Type = T.self
) throws -> T {
guard let parsedValue = T(String(value)) else {
guard let parsedValue = try T(String(value)) else {
throw DecodingError.typeMismatch(
T.self,
.init(
Expand All @@ -89,6 +90,10 @@ extension URISingleValueDecodingContainer {

extension URISingleValueDecodingContainer: SingleValueDecodingContainer {

var codingPath: [any CodingKey] {
decoder.codingPath
}

func decodeNil() -> Bool {
false
}
Expand All @@ -98,7 +103,7 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer {
}

func decode(_ type: String.Type) throws -> String {
String(value)
try String(value)
}

func decode(_ type: Double.Type) throws -> Double {
Expand Down Expand Up @@ -180,9 +185,9 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer {
case is UInt64.Type:
return try decode(UInt64.self) as! T
case is Date.Type:
return try dateTranscoder.decode(String(value)) as! T
return try decoder.dateTranscoder.decode(String(value)) as! T
default:
throw URIValueFromNodeDecoder.GeneralError.unsupportedType(T.self)
return try T.init(from: decoder)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,6 @@ extension URIValueFromNodeDecoder {
/// A decoder error.
enum GeneralError: Swift.Error {

/// The decoder does not support the provided type.
case unsupportedType(Any.Type)

/// The decoder was asked to create a nested container.
case nestedContainersNotSupported

Expand Down Expand Up @@ -274,7 +271,7 @@ extension URIValueFromNodeDecoder {
/// Extracts the node at the top of the coding stack and tries to treat it
/// as a primitive value.
/// - Returns: The value if it can be treated as a primitive value.
private func currentElementAsSingleValue() throws -> URIParsedValue {
func currentElementAsSingleValue() throws -> URIParsedValue {
try nodeAsSingleValue(currentElement)
}

Expand Down Expand Up @@ -368,11 +365,6 @@ extension URIValueFromNodeDecoder: Decoder {
}

func singleValueContainer() throws -> any SingleValueDecodingContainer {
let value = try currentElementAsSingleValue()
return URISingleValueDecodingContainer(
dateTranscoder: dateTranscoder,
codingPath: codingPath,
value: value
)
return URISingleValueDecodingContainer(decoder: self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ extension URISingleValueEncodingContainer: SingleValueEncodingContainer {
case let value as Date:
try _setValue(.date(value))
default:
throw URIValueToNodeEncoder.GeneralError.nestedValueInSingleValueContainer
try value.encode(to: encoder)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ final class URIValueToNodeEncoder {

/// The encoder set a value for an index out of range of the container.
case integerOutOfRange

/// The encoder tried to treat
case nestedValueInSingleValueContainer
}

/// The stack of nested values within the root node.
Expand Down
98 changes: 97 additions & 1 deletion Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
//
//===----------------------------------------------------------------------===//
import XCTest
@testable import OpenAPIRuntime
@_spi(Generated)@testable import OpenAPIRuntime
#if os(Linux)
@preconcurrency import Foundation
#endif

final class Test_URICodingRoundtrip: Test_Runtime {

Expand All @@ -27,12 +30,60 @@ final class Test_URICodingRoundtrip: Test_Runtime {
var maybeFoo: String?
}

struct TrivialStruct: Codable, Equatable {
var foo: String
}

enum SimpleEnum: String, Codable, Equatable {
case red
case green
case blue
}

struct AnyOf: Codable, Equatable, Sendable {
var value1: Foundation.Date?
var value2: SimpleEnum?
var value3: TrivialStruct?
init(value1: Foundation.Date? = nil, value2: SimpleEnum? = nil, value3: TrivialStruct? = nil) {
self.value1 = value1
self.value2 = value2
self.value3 = value3
}
init(from decoder: any Decoder) throws {
do {
let container = try decoder.singleValueContainer()
value1 = try? container.decode(Foundation.Date.self)
}
do {
let container = try decoder.singleValueContainer()
value2 = try? container.decode(SimpleEnum.self)
}
do {
let container = try decoder.singleValueContainer()
value3 = try? container.decode(TrivialStruct.self)
}
try DecodingError.verifyAtLeastOneSchemaIsNotNil(
[value1, value2, value3],
type: Self.self,
codingPath: decoder.codingPath
)
}
func encode(to encoder: any Encoder) throws {
if let value1 {
var container = encoder.singleValueContainer()
try container.encode(value1)
}
if let value2 {
var container = encoder.singleValueContainer()
try container.encode(value2)
}
if let value3 {
var container = encoder.singleValueContainer()
try container.encode(value3)
}
}
}

// An empty string.
try _test(
"",
Expand Down Expand Up @@ -210,6 +261,51 @@ final class Test_URICodingRoundtrip: Test_Runtime {
)
)

// A struct with a custom Codable implementation that forwards
// decoding to nested values.
try _test(
AnyOf(
value1: Date(timeIntervalSince1970: 1_674_036_251)
),
key: "root",
.init(
formExplode: "root=2023-01-18T10%3A04%3A11Z",
formUnexplode: "root=2023-01-18T10%3A04%3A11Z",
simpleExplode: "2023-01-18T10%3A04%3A11Z",
simpleUnexplode: "2023-01-18T10%3A04%3A11Z",
formDataExplode: "root=2023-01-18T10%3A04%3A11Z",
formDataUnexplode: "root=2023-01-18T10%3A04%3A11Z"
)
)
try _test(
AnyOf(
value2: .green
),
key: "root",
.init(
formExplode: "root=green",
formUnexplode: "root=green",
simpleExplode: "green",
simpleUnexplode: "green",
formDataExplode: "root=green",
formDataUnexplode: "root=green"
)
)
try _test(
AnyOf(
value3: .init(foo: "bar")
),
key: "root",
.init(
formExplode: "foo=bar",
formUnexplode: "root=foo,bar",
simpleExplode: "foo=bar",
simpleUnexplode: "foo,bar",
formDataExplode: "foo=bar",
formDataUnexplode: "root=foo,bar"
)
)

// An empty struct.
struct EmptyStruct: Codable, Equatable {}
try _test(
Expand Down

0 comments on commit f4f5963

Please sign in to comment.