Skip to content

Commit b03a4d5

Browse files
committed
refactor: extract KeyedDecodingContainer+DefaultCodable extension
1 parent 07ee7df commit b03a4d5

2 files changed

Lines changed: 195 additions & 184 deletions

File tree

Sources/KarrotCodableKit/BetterCodable/Defaults/DefaultCodable.swift

Lines changed: 0 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -110,187 +110,3 @@ extension DefaultCodable: Hashable where Default.DefaultValue: Hashable {
110110
}
111111

112112
extension DefaultCodable: Sendable where Default.DefaultValue: Sendable {}
113-
114-
// MARK: - KeyedDecodingContainer
115-
116-
public protocol BoolCodableStrategy: DefaultCodableStrategy where DefaultValue == Bool {}
117-
118-
extension KeyedDecodingContainer {
119-
/// Default implementation of decoding a DefaultCodable
120-
///
121-
/// Decodes successfully if key is available if not fallback to the default value provided.
122-
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
123-
// Check if key exists
124-
if !contains(key) {
125-
#if DEBUG
126-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
127-
#else
128-
return DefaultCodable(wrappedValue: P.defaultValue)
129-
#endif
130-
}
131-
132-
// Check for nil
133-
if (try? decodeNil(forKey: key)) == true {
134-
#if DEBUG
135-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
136-
#else
137-
return DefaultCodable(wrappedValue: P.defaultValue)
138-
#endif
139-
}
140-
141-
// Try to decode normally
142-
if let value = try decodeIfPresent(DefaultCodable<P>.self, forKey: key) {
143-
return value
144-
} else {
145-
#if DEBUG
146-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
147-
#else
148-
return DefaultCodable(wrappedValue: P.defaultValue)
149-
#endif
150-
}
151-
}
152-
153-
/// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`.
154-
///
155-
/// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String`
156-
/// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which
157-
/// the data provider might be sending the value as different types. If everything fails defaults to
158-
/// the `defaultValue` provided by the strategy.
159-
public func decode<P: BoolCodableStrategy>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
160-
// Check if key exists
161-
if !contains(key) {
162-
#if DEBUG
163-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
164-
#else
165-
return DefaultCodable(wrappedValue: P.defaultValue)
166-
#endif
167-
}
168-
169-
// Check for nil first
170-
if (try? decodeNil(forKey: key)) == true {
171-
#if DEBUG
172-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
173-
#else
174-
return DefaultCodable(wrappedValue: P.defaultValue)
175-
#endif
176-
}
177-
178-
do {
179-
let value = try decode(Bool.self, forKey: key)
180-
return DefaultCodable(wrappedValue: value)
181-
} catch {
182-
guard
183-
let decodingError = error as? DecodingError,
184-
case .typeMismatch = decodingError
185-
else {
186-
// Report error and use default
187-
#if DEBUG
188-
let decoder = try? superDecoder(forKey: key)
189-
decoder?.reportError(error)
190-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
191-
#else
192-
return DefaultCodable(wrappedValue: P.defaultValue)
193-
#endif
194-
}
195-
if
196-
let intValue = try? decodeIfPresent(Int.self, forKey: key),
197-
let bool = Bool(exactly: NSNumber(value: intValue))
198-
{
199-
return DefaultCodable(wrappedValue: bool)
200-
} else if
201-
let stringValue = try? decodeIfPresent(String.self, forKey: key),
202-
let bool = Bool(stringValue)
203-
{
204-
return DefaultCodable(wrappedValue: bool)
205-
} else {
206-
// Type mismatch - report error
207-
#if DEBUG
208-
let decoder = try? superDecoder(forKey: key)
209-
decoder?.reportError(decodingError)
210-
return DefaultCodable(
211-
wrappedValue: P.defaultValue,
212-
outcome: .recoveredFrom(decodingError, wasReported: decoder != nil)
213-
)
214-
#else
215-
return DefaultCodable(wrappedValue: P.defaultValue)
216-
#endif
217-
}
218-
}
219-
}
220-
221-
/// Decodes a DefaultCodable where the strategy's DefaultValue is RawRepresentable
222-
///
223-
/// This method provides special handling for RawRepresentable types:
224-
/// - If `isFrozen` is false (default), unknown raw values result in UnknownNovelValueError and use the default value
225-
/// - If `isFrozen` is true, unknown raw values result in DecodingError and use the default value
226-
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P>
227-
where P.DefaultValue: RawRepresentable, P.DefaultValue.RawValue: Decodable
228-
{
229-
// Check if key exists
230-
if !contains(key) {
231-
#if DEBUG
232-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
233-
#else
234-
return DefaultCodable(wrappedValue: P.defaultValue)
235-
#endif
236-
}
237-
238-
// Check for nil
239-
if (try? decodeNil(forKey: key)) == true {
240-
#if DEBUG
241-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
242-
#else
243-
return DefaultCodable(wrappedValue: P.defaultValue)
244-
#endif
245-
}
246-
247-
// Try to decode the raw value
248-
do {
249-
let rawValue = try decode(P.DefaultValue.RawValue.self, forKey: key)
250-
251-
// Try to create the enum from raw value
252-
if let value = P.DefaultValue(rawValue: rawValue) {
253-
return DefaultCodable(wrappedValue: value)
254-
} else {
255-
#if DEBUG
256-
/// Unknown raw value
257-
let error = Self.createUnknownRawValueError(
258-
for: P.DefaultValue.self,
259-
rawValue: rawValue,
260-
codingPath: codingPath + [key],
261-
isFrozen: P.isFrozen
262-
)
263-
264-
let decoder = try? superDecoder(forKey: key)
265-
decoder?.reportError(error)
266-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
267-
#else
268-
return DefaultCodable(wrappedValue: P.defaultValue)
269-
#endif
270-
}
271-
} catch {
272-
#if DEBUG
273-
/// Decoding the raw value failed (e.g., type mismatch)
274-
let decoder = try? superDecoder(forKey: key)
275-
decoder?.reportError(error)
276-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
277-
#else
278-
return DefaultCodable(wrappedValue: P.defaultValue)
279-
#endif
280-
}
281-
}
282-
283-
private static func createUnknownRawValueError<T: RawRepresentable>(
284-
for type: T.Type,
285-
rawValue: T.RawValue,
286-
codingPath: [CodingKey],
287-
isFrozen: Bool
288-
) -> Error {
289-
guard isFrozen else { return UnknownNovelValueError(novelValue: rawValue) }
290-
let context = DecodingError.Context(
291-
codingPath: codingPath,
292-
debugDescription: "Cannot initialize \(type) from invalid raw value \(rawValue)"
293-
)
294-
return DecodingError.dataCorrupted(context)
295-
}
296-
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//
2+
// KeyedDecodingContainer+DefaultCodable.swift
3+
// KarrotCodableKit
4+
//
5+
// Created by elon on 9/23/25.
6+
//
7+
8+
import Foundation
9+
10+
public protocol BoolCodableStrategy: DefaultCodableStrategy where DefaultValue == Bool {}
11+
12+
extension KeyedDecodingContainer {
13+
/// Default implementation of decoding a DefaultCodable
14+
///
15+
/// Decodes successfully if key is available if not fallback to the default value provided.
16+
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
17+
// Check if key exists
18+
if !contains(key) {
19+
#if DEBUG
20+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
21+
#else
22+
return DefaultCodable(wrappedValue: P.defaultValue)
23+
#endif
24+
}
25+
26+
// Check for nil
27+
if (try? decodeNil(forKey: key)) == true {
28+
#if DEBUG
29+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
30+
#else
31+
return DefaultCodable(wrappedValue: P.defaultValue)
32+
#endif
33+
}
34+
35+
// Try to decode normally
36+
return try decode(DefaultCodable<P>.self, forKey: key)
37+
}
38+
39+
/// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`.
40+
///
41+
/// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String`
42+
/// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which
43+
/// the data provider might be sending the value as different types. If everything fails defaults to
44+
/// the `defaultValue` provided by the strategy.
45+
public func decode<P: BoolCodableStrategy>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
46+
// Check if key exists
47+
if !contains(key) {
48+
#if DEBUG
49+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
50+
#else
51+
return DefaultCodable(wrappedValue: P.defaultValue)
52+
#endif
53+
}
54+
55+
// Check for nil first
56+
if (try? decodeNil(forKey: key)) == true {
57+
#if DEBUG
58+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
59+
#else
60+
return DefaultCodable(wrappedValue: P.defaultValue)
61+
#endif
62+
}
63+
64+
do {
65+
let value = try decode(Bool.self, forKey: key)
66+
return DefaultCodable(wrappedValue: value)
67+
} catch {
68+
guard
69+
let decodingError = error as? DecodingError,
70+
case .typeMismatch = decodingError
71+
else {
72+
// Report error and use default
73+
#if DEBUG
74+
let decoder = try? superDecoder(forKey: key)
75+
decoder?.reportError(error)
76+
return DefaultCodable(
77+
wrappedValue: P.defaultValue,
78+
outcome: .recoveredFrom(error, wasReported: decoder != nil),
79+
)
80+
#else
81+
return DefaultCodable(wrappedValue: P.defaultValue)
82+
#endif
83+
}
84+
85+
if
86+
let intValue = try? decodeIfPresent(Int.self, forKey: key),
87+
let bool = Bool(exactly: NSNumber(value: intValue))
88+
{
89+
return DefaultCodable(wrappedValue: bool)
90+
}
91+
92+
if
93+
let stringValue = try? decodeIfPresent(String.self, forKey: key),
94+
let bool = Bool(stringValue)
95+
{
96+
return DefaultCodable(wrappedValue: bool)
97+
}
98+
99+
// Type mismatch - report error
100+
#if DEBUG
101+
let decoder = try? superDecoder(forKey: key)
102+
decoder?.reportError(decodingError)
103+
return DefaultCodable(
104+
wrappedValue: P.defaultValue,
105+
outcome: .recoveredFrom(decodingError, wasReported: decoder != nil),
106+
)
107+
#else
108+
return DefaultCodable(wrappedValue: P.defaultValue)
109+
#endif
110+
}
111+
}
112+
113+
/// Decodes a DefaultCodable where the strategy's DefaultValue is RawRepresentable
114+
///
115+
/// This method provides special handling for RawRepresentable types:
116+
/// - If `isFrozen` is false (default), unknown raw values result in UnknownNovelValueError and use the default value
117+
/// - If `isFrozen` is true, unknown raw values result in DecodingError and use the default value
118+
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P>
119+
where P.DefaultValue: RawRepresentable, P.DefaultValue.RawValue: Decodable
120+
{
121+
// Check if key exists
122+
if !contains(key) {
123+
#if DEBUG
124+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .keyNotFound)
125+
#else
126+
return DefaultCodable(wrappedValue: P.defaultValue)
127+
#endif
128+
}
129+
130+
// Check for nil
131+
if (try? decodeNil(forKey: key)) == true {
132+
#if DEBUG
133+
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .valueWasNil)
134+
#else
135+
return DefaultCodable(wrappedValue: P.defaultValue)
136+
#endif
137+
}
138+
139+
// Try to decode the raw value
140+
do {
141+
let rawValue = try decode(P.DefaultValue.RawValue.self, forKey: key)
142+
143+
// Try to create the enum from raw value
144+
if let value = P.DefaultValue(rawValue: rawValue) {
145+
return DefaultCodable(wrappedValue: value)
146+
}
147+
148+
#if DEBUG
149+
/// Unknown raw value
150+
let error = Self.createUnknownRawValueError(
151+
for: P.DefaultValue.self,
152+
rawValue: rawValue,
153+
codingPath: codingPath + [key],
154+
isFrozen: P.isFrozen,
155+
)
156+
157+
let decoder = try? superDecoder(forKey: key)
158+
decoder?.reportError(error)
159+
return DefaultCodable(
160+
wrappedValue: P.defaultValue,
161+
outcome: .recoveredFrom(error, wasReported: decoder != nil)
162+
)
163+
#else
164+
return DefaultCodable(wrappedValue: P.defaultValue)
165+
#endif
166+
167+
} catch {
168+
#if DEBUG
169+
/// Decoding the raw value failed (e.g., type mismatch)
170+
let decoder = try? superDecoder(forKey: key)
171+
decoder?.reportError(error)
172+
return DefaultCodable(
173+
wrappedValue: P.defaultValue,
174+
outcome: .recoveredFrom(error, wasReported: decoder != nil)
175+
)
176+
#else
177+
return DefaultCodable(wrappedValue: P.defaultValue)
178+
#endif
179+
}
180+
}
181+
182+
private static func createUnknownRawValueError<T: RawRepresentable>(
183+
for type: T.Type,
184+
rawValue: T.RawValue,
185+
codingPath: [CodingKey],
186+
isFrozen: Bool,
187+
) -> Error {
188+
guard isFrozen else { return UnknownNovelValueError(novelValue: rawValue) }
189+
let context = DecodingError.Context(
190+
codingPath: codingPath,
191+
debugDescription: "Cannot initialize \(type) from invalid raw value \(rawValue)",
192+
)
193+
return DecodingError.dataCorrupted(context)
194+
}
195+
}

0 commit comments

Comments
 (0)