Skip to content

Commit 0ef6569

Browse files
committed
KeyDecodingStrategy
1 parent a5e7923 commit 0ef6569

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

Sources/KeyValueDecoder.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public struct KeyValueDecoder: Sendable {
4444
/// The strategy to use for decoding BinaryInteger types. Defaults to `.exact` for lossless conversion between types.
4545
public var intDecodingStrategy: IntDecodingStrategy = .exact
4646

47+
/// The strategy to use for decoding each types keys.
48+
public var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
49+
4750
/// Initializes `self` with default strategy.
4851
public init () {
4952
self.userInfo = [:]
@@ -82,6 +85,15 @@ public struct KeyValueDecoder: Sendable {
8285
/// Floating point conversions are also clamped, rounded when a rule is provided
8386
case clamping(roundingRule: FloatingPointRoundingRule?)
8487
}
88+
89+
/// Strategy to determine how to decode a type’s coding keys from String values.
90+
public enum KeyDecodingStrategy: Sendable {
91+
/// A key decoding strategy that converts snake-case keys to camel-case keys.
92+
case convertFromSnakeCase
93+
94+
/// A key encoding strategy that doesn’t change key names during encoding.
95+
case useDefaultKeys
96+
}
8597
}
8698

8799
#if canImport(Combine)
@@ -105,12 +117,14 @@ private extension KeyValueDecoder {
105117
struct DecodingStrategy {
106118
var optionals: NilDecodingStrategy
107119
var integers: IntDecodingStrategy
120+
var keys: KeyDecodingStrategy
108121
}
109122

110123
var strategy: DecodingStrategy {
111124
DecodingStrategy(
112125
optionals: nilDecodingStrategy,
113-
integers: intDecodingStrategy
126+
integers: intDecodingStrategy,
127+
keys: keyDecodingStrategy
114128
)
115129
}
116130

@@ -381,7 +395,8 @@ private extension KeyValueDecoder {
381395

382396
func container(for key: Key) throws -> SingleContainer {
383397
let path = codingPath.appending(key: key)
384-
guard let value = storage[key.stringValue] else {
398+
let kkk = strategy.keys.makeStorageKey(for: key.stringValue)
399+
guard let value = storage[kkk] else {
385400
let keyPath = codingPath.makeKeyPath(appending: key)
386401
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Dictionary does not contain key \(keyPath)")
387402
throw DecodingError.keyNotFound(key, context)
@@ -395,7 +410,7 @@ private extension KeyValueDecoder {
395410
}
396411

397412
func contains(_ key: Key) -> Bool {
398-
return storage[key.stringValue] != nil
413+
return storage[strategy.keys.makeStorageKey(for: key.stringValue)] != nil
399414
}
400415

401416
func decodeNil(forKey key: Key) throws -> Bool {
@@ -640,6 +655,16 @@ private extension KeyValueDecoder {
640655
}
641656
}
642657

658+
extension KeyValueDecoder.KeyDecodingStrategy {
659+
660+
func makeStorageKey(for key: String) -> String {
661+
switch self {
662+
case .useDefaultKeys: return key
663+
case .convertFromSnakeCase: return key.toSnakeCase()
664+
}
665+
}
666+
}
667+
643668
extension BinaryInteger {
644669

645670
init?(from source: Double, using strategy: KeyValueDecoder.IntDecodingStrategy) {

Tests/KeyValueDecoderTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,28 @@ struct KeyValueDecoderTests {
448448
}
449449
}
450450

451+
@Test
452+
func decodes_SnakeCase() throws {
453+
let dict: [String: Any] = [
454+
"first_name": "fish",
455+
"surname": "chips",
456+
"profile_url": "drop",
457+
"rel_nodes_link": ["ocean": ["first_name": "shrimp", "surname": "anemone"]]
458+
]
459+
460+
var decoder = KeyValueDecoder()
461+
decoder.keyDecodingStrategy = .convertFromSnakeCase
462+
463+
#expect(
464+
try decoder.decode(SnakeNode.self, from: dict) == SnakeNode(
465+
firstName: "fish",
466+
lastName: "chips",
467+
profileURL: "drop",
468+
relNODESLink: ["ocean": SnakeNode(firstName: "shrimp", lastName: "anemone")]
469+
)
470+
)
471+
}
472+
451473
@Test
452474
func decodes_NestedUnkeyed() throws {
453475
let decoder = KeyValueDecoder()

0 commit comments

Comments
 (0)