diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index e0b593a..23469ab 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -96,6 +96,10 @@ extension JSONDecoder.DateDecodingStrategy { } } +public protocol DecodingErrorHandler: Sendable { + func willThrow(_ error: any Error) +} + /// A set of configuration values used by the generated client and server types. public struct Configuration: Sendable { @@ -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: @@ -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 ) { self.dateTranscoder = dateTranscoder self.multipartBoundaryGenerator = multipartBoundaryGenerator + self.decodingErrorHandler = decodingErrorHandler } } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 38a1711..988bba9 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -132,7 +132,14 @@ extension Converter { /// - Throws: An error if decoding from the body fails. func convertJSONToBodyCodable(_ 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. diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 4a7b669..ee2b540 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -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( @@ -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() + } +}