Skip to content

[Runtime] Include partial errors in oneOf/anyOf decoding errors #66

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
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
76 changes: 67 additions & 9 deletions Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
}
Expand All @@ -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.
Expand All @@ -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
}
}
71 changes: 71 additions & 0 deletions Sources/OpenAPIRuntime/Deprecated/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
}
16 changes: 12 additions & 4 deletions Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down