diff --git a/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift b/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift index ff41be62..20b7a76a 100644 --- a/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift @@ -22,16 +22,19 @@ extension DecodingError { /// occurred. /// - codingPath: The coding path to the decoder that attempted to decode /// the type. + /// - errors: The errors encountered when decoding individual cases. /// - Returns: A decoding error. static func failedToDecodeAnySchema( type: Any.Type, - codingPath: [any CodingKey] + codingPath: [any CodingKey], + errors: [any Error] ) -> Self { DecodingError.valueNotFound( type, DecodingError.Context.init( codingPath: codingPath, - debugDescription: "The anyOf structure did not decode into any child schema." + debugDescription: "The anyOf structure did not decode into any child schema.", + underlyingError: MultiError(errors: errors) ) ) } @@ -43,24 +46,47 @@ extension DecodingError { /// occurred. /// - codingPath: The coding path to the decoder that attempted to decode /// the type. + /// - errors: The errors encountered when decoding individual cases. /// - Returns: A decoding error. @_spi(Generated) public static func failedToDecodeOneOfSchema( type: Any.Type, - codingPath: [any CodingKey] + codingPath: [any CodingKey], + errors: [any Error] ) -> Self { DecodingError.valueNotFound( type, DecodingError.Context.init( codingPath: codingPath, - debugDescription: "The oneOf structure did not decode into any child schema." + debugDescription: "The oneOf structure did not decode into any child schema.", + underlyingError: MultiError(errors: errors) ) ) } -} -@_spi(Generated) -extension DecodingError { + /// Returns a decoding error used by the oneOf decoder when + /// the discriminator property contains an unknown schema name. + /// - Parameters: + /// - discriminatorKey: The discriminator coding key. + /// - discriminatorValue: The unknown value of the discriminator. + /// - codingPath: The coding path to the decoder that attempted to decode + /// the type, with the discriminator value as the last component. + /// - Returns: A decoding error. + @_spi(Generated) + public static func unknownOneOfDiscriminator( + discriminatorKey: any CodingKey, + discriminatorValue: String, + codingPath: [any CodingKey] + ) -> Self { + return DecodingError.keyNotFound( + discriminatorKey, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: + "The oneOf structure does not contain the provided discriminator value '\(discriminatorValue)'." + ) + ) + } /// Verifies that the anyOf decoder successfully decoded at least one /// child schema, and throws an error otherwise. @@ -70,17 +96,49 @@ extension DecodingError { /// occurred. /// - codingPath: The coding path to the decoder that attempted to decode /// the type. + /// - errors: The errors encountered when decoding individual cases. /// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded. + @_spi(Generated) public static func verifyAtLeastOneSchemaIsNotNil( _ values: [Any?], type: Any.Type, - codingPath: [any CodingKey] + codingPath: [any CodingKey], + errors: [any Error] ) throws { guard values.contains(where: { $0 != nil }) else { throw DecodingError.failedToDecodeAnySchema( type: type, - codingPath: codingPath + codingPath: codingPath, + errors: errors ) } } } + +/// A wrapper of multiple errors, for example collected during a parallelized +/// operation from the individual subtasks. +struct MultiError: Swift.Error, LocalizedError, CustomStringConvertible { + + /// The multiple underlying errors. + var errors: [any Error] + + var description: String { + let combinedDescription = + errors + .map { error in + guard let error = error as? (any PrettyStringConvertible) else { + return error.localizedDescription + } + return error.prettyDescription + } + .enumerated() + .map { ($0.offset + 1, $0.element) } + .map { "Error \($0.0): [\($0.1)]" } + .joined(separator: ", ") + return "MultiError (contains \(errors.count) error\(errors.count == 1 ? "" : "s")): \(combinedDescription)" + } + + var errorDescription: String? { + description + } +} diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index c6d1d1af..5de87792 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -143,3 +143,74 @@ extension Converter { } } } + +extension DecodingError { + /// Returns a decoding error used by the oneOf decoder when not a single + /// child schema decodes the received payload. + /// - Parameters: + /// - type: The type representing the oneOf schema in which the decoding + /// occurred. + /// - codingPath: The coding path to the decoder that attempted to decode + /// the type. + /// - Returns: A decoding error. + @_spi(Generated) + @available(*, deprecated) + public static func failedToDecodeOneOfSchema( + type: Any.Type, + codingPath: [any CodingKey] + ) -> Self { + DecodingError.valueNotFound( + type, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "The oneOf structure did not decode into any child schema." + ) + ) + } + + /// Returns a decoding error used by the anyOf decoder when not a single + /// child schema decodes the received payload. + /// - Parameters: + /// - type: The type representing the anyOf schema in which the decoding + /// occurred. + /// - codingPath: The coding path to the decoder that attempted to decode + /// the type. + /// - Returns: A decoding error. + @available(*, deprecated) + static func failedToDecodeAnySchema( + type: Any.Type, + codingPath: [any CodingKey] + ) -> Self { + DecodingError.valueNotFound( + type, + DecodingError.Context.init( + codingPath: codingPath, + debugDescription: "The anyOf structure did not decode into any child schema." + ) + ) + } + + /// Verifies that the anyOf decoder successfully decoded at least one + /// child schema, and throws an error otherwise. + /// - Parameters: + /// - values: An array of optional values to check. + /// - type: The type representing the anyOf schema in which the decoding + /// occurred. + /// - codingPath: The coding path to the decoder that attempted to decode + /// the type. + /// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded. + @_spi(Generated) + @available(*, deprecated) + public static func verifyAtLeastOneSchemaIsNotNil( + _ values: [Any?], + type: Any.Type, + codingPath: [any CodingKey] + ) throws { + guard values.contains(where: { $0 != nil }) else { + throw DecodingError.failedToDecodeAnySchema( + type: type, + codingPath: codingPath + ) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index fcce3775..44c62520 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -50,22 +50,30 @@ final class Test_URICodingRoundtrip: Test_Runtime { self.value3 = value3 } init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { let container = try decoder.singleValueContainer() - value1 = try? container.decode(Foundation.Date.self) + value1 = try container.decode(Foundation.Date.self) + } catch { + errors.append(error) } do { let container = try decoder.singleValueContainer() - value2 = try? container.decode(SimpleEnum.self) + value2 = try container.decode(SimpleEnum.self) + } catch { + errors.append(error) } do { let container = try decoder.singleValueContainer() - value3 = try? container.decode(TrivialStruct.self) + value3 = try container.decode(TrivialStruct.self) + } catch { + errors.append(error) } try DecodingError.verifyAtLeastOneSchemaIsNotNil( [value1, value2, value3], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } func encode(to encoder: any Encoder) throws {