diff --git a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift index fb59fb4b..13a78a48 100644 --- a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift @@ -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( + _ 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 @@ -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( + _ 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. diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 5c3e0ad2..3929df11 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -17,18 +17,19 @@ 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. @@ -36,7 +37,7 @@ extension URISingleValueDecodingContainer { private func _decodeBinaryFloatingPoint( _: 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( @@ -55,7 +56,7 @@ extension URISingleValueDecodingContainer { private func _decodeFixedWidthInteger( _: 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( @@ -74,7 +75,7 @@ extension URISingleValueDecodingContainer { private func _decodeLosslessStringConvertible( _: 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( @@ -89,6 +90,10 @@ extension URISingleValueDecodingContainer { extension URISingleValueDecodingContainer: SingleValueDecodingContainer { + var codingPath: [any CodingKey] { + decoder.codingPath + } + func decodeNil() -> Bool { false } @@ -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 { @@ -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) } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index ec209b81..472aed8b 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -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 @@ -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) } @@ -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) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index ca3af754..a0da53bd 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -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) } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index 919de179..fb227794 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -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. diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 97572042..c8c93e1d 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -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 { @@ -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( "", @@ -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(