Skip to content
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

Add a hook for intercepting decoding errors #99

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
10 changes: 9 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ extension JSONDecoder.DateDecodingStrategy {
}
}

public protocol DecodingErrorHandler: Sendable {
func willThrow(_ error: any Error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be interested in the context this was thrown in? See ClientError and ServerError, where we add more context.

}

/// A set of configuration values used by the generated client and server types.
public struct Configuration: Sendable {

Expand All @@ -105,6 +109,8 @@ public struct Configuration: Sendable {
/// The generator to use when creating mutlipart bodies.
public var multipartBoundaryGenerator: any MultipartBoundaryGenerator

public var decodingErrorHandler: (any DecodingErrorHandler)?

/// Creates a new configuration with the specified values.
///
/// - Parameters:
Expand All @@ -113,9 +119,11 @@ public struct Configuration: Sendable {
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
public init(
dateTranscoder: any DateTranscoder = .iso8601,
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
decodingErrorHandler: (any DecodingErrorHandler)? = nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, please just add a deprecated 2-parameter version of this initializer in Deprecated.swift, as adding a param is technically a source break. Similar to #102 (comment)

) {
self.dateTranscoder = dateTranscoder
self.multipartBoundaryGenerator = multipartBoundaryGenerator
self.decodingErrorHandler = decodingErrorHandler
}
}
9 changes: 8 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,14 @@ extension Converter {
/// - Throws: An error if decoding from the body fails.
func convertJSONToBodyCodable<T: Decodable>(_ body: HTTPBody) async throws -> T {
let data = try await Data(collecting: body, upTo: .max)
return try decoder.decode(T.self, from: data)
do {
return try decoder.decode(T.self, from: data)
} catch {
if let decodingErrorHandler = configuration.decodingErrorHandler {
decodingErrorHandler.willThrow(error)
}
throw error
}
}

/// Returns a JSON body for the provided encodable value.
Expand Down
51 changes: 51 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,40 @@ final class Test_ClientConverterExtensions: Test_Runtime {
XCTAssertEqual(value, testStruct)
}

func testDecodingErrorHandler() async throws {
let decodingErrorHandler = TestDecodingErrorHandler()
let converter = Converter(
configuration: Configuration(
decodingErrorHandler: decodingErrorHandler
)
)
do {
_ = try await converter.getResponseBodyAsJSON(
TestPetDetailed.self,
from: .init(testStructData),
transforming: { $0 }
)
XCTFail("Unreachable")
} catch {
XCTAssertEqual(decodingErrorHandler.errorsThrown.count, 1)
let interceptedError = try XCTUnwrap(decodingErrorHandler.errorsThrown.first as? DecodingError)
switch interceptedError {
case .typeMismatch, .valueNotFound, .dataCorrupted:
XCTFail("Unreachable")
case .keyNotFound(let key, let context):
XCTAssertEqual(key.stringValue, "type")
XCTAssertEqual(
context.debugDescription,
"""
No value associated with key CodingKeys(stringValue: "type", intValue: nil) ("type").
"""
)
@unknown default:
XCTFail("Unreachable")
}
}
}

// | client | get | response body | binary | required | getResponseBodyAsBinary |
func test_getResponseBodyAsBinary_data() async throws {
let value: HTTPBody = try converter.getResponseBodyAsBinary(
Expand Down Expand Up @@ -256,3 +290,20 @@ public func XCTAssertEqualStringifiedData(
XCTAssertEqual(actualString, try expression2(), file: file, line: line)
} catch { XCTFail(error.localizedDescription, file: file, line: line) }
}

final class TestDecodingErrorHandler: DecodingErrorHandler, @unchecked Sendable {
private let lock = NSLock()
private var _errorsThrown = [any Error]()

var errorsThrown: [any Error] {
lock.lock()
defer { lock.unlock() }
return _errorsThrown
}

func willThrow(_ error: any Error) {
lock.lock()
_errorsThrown.append(error)
lock.unlock()
}
}