From e3593a117aa164725b2fed793686f57a6b4880fa Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Wed, 23 Sep 2026 12:47:31 +0200 Subject: [PATCH] Add blob data strategy that keeps Data as is --- .../Decoder/DictionaryComponentDecoder.swift | 3 ++ .../DictionaryDataDecodingStrategy.swift | 3 ++ .../Encoder/DictionaryComponentEncoder.swift | 3 ++ .../DictionaryDataEncodingStrategy.swift | 1 + .../DictionaryDecoderStrategiesTests.swift | 32 +++++++++++++++++++ Tests/Decoder/DictionaryDecoderTesting.swift | 10 ++++-- .../DictionaryEncoderStrategiesTests.swift | 8 +++++ Tests/Encoder/DictionaryEncoderTesting.swift | 10 ++++-- 8 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Sources/Decoder/DictionaryComponentDecoder.swift b/Sources/Decoder/DictionaryComponentDecoder.swift index eefea70..be18390 100644 --- a/Sources/Decoder/DictionaryComponentDecoder.swift +++ b/Sources/Decoder/DictionaryComponentDecoder.swift @@ -158,6 +158,9 @@ extension DictionaryComponentDecoder { return data + case .blob: + return try decodePrimitiveValue(from: component, at: codingPath) + case .custom(let closure): return try decodeCustomizedValue(from: component, at: codingPath, closure: closure) } diff --git a/Sources/Decoder/Options/DictionaryDataDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryDataDecodingStrategy.swift index 0570691..feed1aa 100644 --- a/Sources/Decoder/Options/DictionaryDataDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryDataDecodingStrategy.swift @@ -11,6 +11,9 @@ public enum DictionaryDataDecodingStrategy: Sendable { /// The strategy that decodes data using Base 64 decoding. case base64 + /// The strategy that takes data stored in the dictionary as is. + case blob + /// The strategy that decodes data using a user-defined function. case custom(@Sendable (_ decoder: Decoder) throws -> Data) } diff --git a/Sources/Encoder/DictionaryComponentEncoder.swift b/Sources/Encoder/DictionaryComponentEncoder.swift index f9198e8..a6f4627 100644 --- a/Sources/Encoder/DictionaryComponentEncoder.swift +++ b/Sources/Encoder/DictionaryComponentEncoder.swift @@ -100,6 +100,9 @@ extension DictionaryComponentEncoder { case .base64: return encodePrimitiveValue(data.base64EncodedString(), at: codingPath) + case .blob: + return encodePrimitiveValue(data, at: codingPath) + case let .custom(closure): return try encodeCustomizedValue(data, at: codingPath, closure: closure) } diff --git a/Sources/Encoder/Options/DictionaryDataEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryDataEncodingStrategy.swift index 0c851f4..2581970 100644 --- a/Sources/Encoder/Options/DictionaryDataEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryDataEncodingStrategy.swift @@ -6,5 +6,6 @@ public enum DictionaryDataEncodingStrategy: Sendable { case deferredToData case base64 + case blob case custom(@Sendable (_ data: Data, _ encoder: Encoder) throws -> Void) } diff --git a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift index b6cb052..fffb9ba 100644 --- a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift +++ b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift @@ -216,6 +216,38 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti } } + func testThatDecoderSucceedsWhenDecodingDataToBlob() { + decoder.dataDecodingStrategy = .blob + + let dictionary: [String: Any] = [ + "foo": Data([1, 2, 3]), + "bar": NSData(data: Data([1, 2, 3])) + ] + + let value = [ + "foo": Data([1, 2, 3]), + "bar": Data([1, 2, 3]) + ] + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderFailsWhenDecodingInvalidDataToBlob() { + decoder.dataDecodingStrategy = .blob + + let dictionary = ["foobar": "AQID"] + + assertDecoderFails(decoding: [String: Data].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Data.Type: + return true + + default: + return false + } + } + } + func testThatDecoderSucceedsWhenDecodingDataUsingCustomFunction() { decoder.dataDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() diff --git a/Tests/Decoder/DictionaryDecoderTesting.swift b/Tests/Decoder/DictionaryDecoderTesting.swift index d2b4776..0e31868 100644 --- a/Tests/Decoder/DictionaryDecoderTesting.swift +++ b/Tests/Decoder/DictionaryDecoderTesting.swift @@ -15,7 +15,10 @@ extension DictionaryDecoderTesting { jsonDecoder.keyDecodingStrategy = decoder.keyDecodingStrategy.jsonDecodingStrategy jsonDecoder.dateDecodingStrategy = decoder.dateDecodingStrategy.jsonDecodingStrategy - jsonDecoder.dataDecodingStrategy = decoder.dataDecodingStrategy.jsonDecodingStrategy + jsonDecoder.dataDecodingStrategy = try XCTUnwrap( + decoder.dataDecodingStrategy.jsonDecodingStrategy, + "JSON has no data type, pass the expected value instead." + ) jsonDecoder.nonConformingFloatDecodingStrategy = decoder.nonConformingFloatDecodingStrategy.jsonDecodingStrategy let data = try JSONSerialization.data(withJSONObject: dictionary, options: .fragmentsAllowed) @@ -169,7 +172,7 @@ extension DictionaryDataDecodingStrategy { // MARK: - Instance Properties - fileprivate var jsonDecodingStrategy: JSONDecoder.DataDecodingStrategy { + fileprivate var jsonDecodingStrategy: JSONDecoder.DataDecodingStrategy? { switch self { case .deferredToData: return .deferredToData @@ -177,6 +180,9 @@ extension DictionaryDataDecodingStrategy { case .base64: return .base64 + case .blob: + return nil + case let .custom(closure): return .custom(closure) } diff --git a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift index 6f0b642..4ec139d 100644 --- a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift +++ b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift @@ -113,6 +113,14 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti assertEncoderSucceeds(encoding: value) } + func testThatEncoderSucceedsWhenEncodingDataToBlob() { + encoder.dataEncodingStrategy = .blob + + let value = ["foobar": Data([1, 2, 3])] + + assertEncoderSucceeds(encoding: value, expecting: value) + } + func testThatEncoderSucceedsWhenEncodingDataUsingCustomFunction() { encoder.dataEncodingStrategy = .custom { data, encoder in var container = encoder.singleValueContainer() diff --git a/Tests/Encoder/DictionaryEncoderTesting.swift b/Tests/Encoder/DictionaryEncoderTesting.swift index 9dcf531..ba17b03 100644 --- a/Tests/Encoder/DictionaryEncoderTesting.swift +++ b/Tests/Encoder/DictionaryEncoderTesting.swift @@ -17,7 +17,10 @@ extension DictionaryEncoderTesting { jsonEncoder.keyEncodingStrategy = encoder.keyEncodingStrategy.jsonEncodingStrategy jsonEncoder.dateEncodingStrategy = encoder.dateEncodingStrategy.jsonEncodingStrategy - jsonEncoder.dataEncodingStrategy = encoder.dataEncodingStrategy.jsonEncodingStrategy + jsonEncoder.dataEncodingStrategy = try XCTUnwrap( + encoder.dataEncodingStrategy.jsonEncodingStrategy, + "JSON has no data type, pass the expected dictionary instead." + ) jsonEncoder.nonConformingFloatEncodingStrategy = encoder.nonConformingFloatEncodingStrategy.jsonEncodingStrategy let data = try jsonEncoder.encode(value) @@ -135,7 +138,7 @@ extension DictionaryDataEncodingStrategy { // MARK: - Instance Properties - fileprivate var jsonEncodingStrategy: JSONEncoder.DataEncodingStrategy { + fileprivate var jsonEncodingStrategy: JSONEncoder.DataEncodingStrategy? { switch self { case .deferredToData: return .deferredToData @@ -143,6 +146,9 @@ extension DictionaryDataEncodingStrategy { case .base64: return .base64 + case .blob: + return nil + case let .custom(closure): return .custom(closure) }