Skip to content

Commit

Permalink
Add a hook for intercepting decoding errors
Browse files Browse the repository at this point in the history
This is just a proof-of-concept, if this direction is reasonable, it
should be fleshed out to handle all decoding paths.

See apple/swift-openapi-generator#522 for
motivation.
  • Loading branch information
jpsim committed Feb 4, 2024
1 parent 76951d7 commit 2dc342d
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 2 deletions.
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)
}

/// 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
) {
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()
}
}

0 comments on commit 2dc342d

Please sign in to comment.