Skip to content

Commit c82af3c

Browse files
authored
Merge pull request #18 from daangn/task/elon/IOS-1332-update-outcome-logic
feat: Add outcome logic for optional property wrappers
2 parents bebc686 + 80347a9 commit c82af3c

9 files changed

Lines changed: 604 additions & 304 deletions

File tree

Sources/KarrotCodableKit/BetterCodable/DateValue/OptionalDateValue.swift

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,15 @@ extension OptionalDateValue: Decodable where Formatter.RawValue: Decodable {
5858
#endif
5959
throw error
6060
}
61+
62+
} catch DecodingError.keyNotFound {
63+
self.wrappedValue = nil
64+
self.outcome = .keyNotFound
65+
6166
} catch DecodingError.valueNotFound(let rawType, _) where rawType == Formatter.RawValue.self {
6267
self.wrappedValue = nil
6368
self.outcome = .valueWasNil
69+
6470
} catch {
6571
#if DEBUG
6672
decoder.reportError(error)
@@ -96,20 +102,38 @@ extension KeyedDecodingContainer {
96102
_ type: OptionalDateValue<T>.Type,
97103
forKey key: Self.Key
98104
) throws -> OptionalDateValue<T> where T.RawValue: Decodable {
99-
try decodeIfPresent(type, forKey: key) ?? OptionalDateValue<T>(wrappedValue: nil)
105+
// Check if the key exists
106+
guard contains(key) else {
107+
return OptionalDateValue<T>(wrappedValue: nil, outcome: .keyNotFound)
108+
}
109+
110+
// Check if the value is null
111+
if try decodeNil(forKey: key) {
112+
return OptionalDateValue<T>(wrappedValue: nil, outcome: .valueWasNil)
113+
}
114+
115+
// Try to decode using the generic approach
116+
let value = try decodeIfPresent(type, forKey: key)
117+
return value ?? OptionalDateValue<T>(wrappedValue: nil, outcome: .keyNotFound)
100118
}
101119

102120
public func decodeIfPresent<T>(
103121
_ type: OptionalDateValue<T>.Type,
104-
forKey key: Self.Key
105-
) throws -> OptionalDateValue<T> where T.RawValue == String {
106-
let stringOptionalValue = try decodeIfPresent(String.self, forKey: key)
122+
forKey key: Self.Key,
123+
) throws -> OptionalDateValue<T>? where T.RawValue == String {
124+
// Check if the key exists
125+
guard contains(key) else {
126+
return nil
127+
}
107128

108-
guard let stringValue = stringOptionalValue else {
109-
return .init(wrappedValue: nil)
129+
// Check if the value is null
130+
if try decodeNil(forKey: key) {
131+
return OptionalDateValue<T>(wrappedValue: nil, outcome: .valueWasNil)
110132
}
111133

134+
// Try to decode the string value
135+
let stringValue = try decode(String.self, forKey: key)
112136
let dateValue = try T.decode(stringValue)
113-
return .init(wrappedValue: dateValue)
137+
return OptionalDateValue<T>(wrappedValue: dateValue, outcome: .decodedSuccessfully)
114138
}
115139
}

Sources/KarrotCodableKit/BetterCodable/Defaults/DefaultCodable.swift

Lines changed: 0 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -110,230 +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-
let context = DecodingError.Context(
127-
codingPath: codingPath + [key],
128-
debugDescription: "Key not found but property is non-optional"
129-
)
130-
let error = DecodingError.keyNotFound(key, context)
131-
let decoder = try? superDecoder(forKey: key)
132-
decoder?.reportError(error)
133-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
134-
#else
135-
return DefaultCodable(wrappedValue: P.defaultValue)
136-
#endif
137-
}
138-
139-
// Check for nil
140-
if (try? decodeNil(forKey: key)) == true {
141-
#if DEBUG
142-
let context = DecodingError.Context(
143-
codingPath: codingPath + [key],
144-
debugDescription: "Value was nil but property is non-optional"
145-
)
146-
let error = DecodingError.valueNotFound(P.DefaultValue.self, context)
147-
let decoder = try? superDecoder(forKey: key)
148-
decoder?.reportError(error)
149-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
150-
#else
151-
return DefaultCodable(wrappedValue: P.defaultValue)
152-
#endif
153-
}
154-
155-
// Try to decode normally
156-
if let value = try decodeIfPresent(DefaultCodable<P>.self, forKey: key) {
157-
return value
158-
} else {
159-
#if DEBUG
160-
let context = DecodingError.Context(
161-
codingPath: codingPath + [key],
162-
debugDescription: "Key not found but property is non-optional"
163-
)
164-
let error = DecodingError.keyNotFound(key, context)
165-
let decoder = try? superDecoder(forKey: key)
166-
decoder?.reportError(error)
167-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
168-
#else
169-
return DefaultCodable(wrappedValue: P.defaultValue)
170-
#endif
171-
}
172-
}
173-
174-
/// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`.
175-
///
176-
/// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String`
177-
/// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which
178-
/// the data provider might be sending the value as different types. If everything fails defaults to
179-
/// the `defaultValue` provided by the strategy.
180-
public func decode<P: BoolCodableStrategy>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
181-
// Check if key exists
182-
if !contains(key) {
183-
#if DEBUG
184-
let context = DecodingError.Context(
185-
codingPath: codingPath + [key],
186-
debugDescription: "Key not found but property is non-optional"
187-
)
188-
let error = DecodingError.keyNotFound(key, context)
189-
let decoder = try? superDecoder(forKey: key)
190-
decoder?.reportError(error)
191-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
192-
#else
193-
return DefaultCodable(wrappedValue: P.defaultValue)
194-
#endif
195-
}
196-
197-
// Check for nil first
198-
if (try? decodeNil(forKey: key)) == true {
199-
#if DEBUG
200-
let context = DecodingError.Context(
201-
codingPath: codingPath + [key],
202-
debugDescription: "Value was nil but property is non-optional"
203-
)
204-
let error = DecodingError.valueNotFound(Bool.self, context)
205-
let decoder = try? superDecoder(forKey: key)
206-
decoder?.reportError(error)
207-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
208-
#else
209-
return DefaultCodable(wrappedValue: P.defaultValue)
210-
#endif
211-
}
212-
213-
do {
214-
let value = try decode(Bool.self, forKey: key)
215-
return DefaultCodable(wrappedValue: value)
216-
} catch {
217-
guard
218-
let decodingError = error as? DecodingError,
219-
case .typeMismatch = decodingError
220-
else {
221-
// Report error and use default
222-
#if DEBUG
223-
let decoder = try? superDecoder(forKey: key)
224-
decoder?.reportError(error)
225-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
226-
#else
227-
return DefaultCodable(wrappedValue: P.defaultValue)
228-
#endif
229-
}
230-
if
231-
let intValue = try? decodeIfPresent(Int.self, forKey: key),
232-
let bool = Bool(exactly: NSNumber(value: intValue))
233-
{
234-
return DefaultCodable(wrappedValue: bool)
235-
} else if
236-
let stringValue = try? decodeIfPresent(String.self, forKey: key),
237-
let bool = Bool(stringValue)
238-
{
239-
return DefaultCodable(wrappedValue: bool)
240-
} else {
241-
// Type mismatch - report error
242-
#if DEBUG
243-
let decoder = try? superDecoder(forKey: key)
244-
decoder?.reportError(decodingError)
245-
return DefaultCodable(
246-
wrappedValue: P.defaultValue,
247-
outcome: .recoveredFrom(decodingError, wasReported: decoder != nil)
248-
)
249-
#else
250-
return DefaultCodable(wrappedValue: P.defaultValue)
251-
#endif
252-
}
253-
}
254-
}
255-
256-
/// Decodes a DefaultCodable where the strategy's DefaultValue is RawRepresentable
257-
///
258-
/// This method provides special handling for RawRepresentable types:
259-
/// - If `isFrozen` is false (default), unknown raw values result in UnknownNovelValueError and use the default value
260-
/// - If `isFrozen` is true, unknown raw values result in DecodingError and use the default value
261-
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P>
262-
where P.DefaultValue: RawRepresentable, P.DefaultValue.RawValue: Decodable
263-
{
264-
// Check if key exists
265-
if !contains(key) {
266-
#if DEBUG
267-
let context = DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Key not found")
268-
let error = DecodingError.keyNotFound(key, context)
269-
let decoder = try? superDecoder(forKey: key)
270-
decoder?.reportError(error)
271-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
272-
#else
273-
return DefaultCodable(wrappedValue: P.defaultValue)
274-
#endif
275-
}
276-
277-
// Check for nil
278-
if (try? decodeNil(forKey: key)) == true {
279-
#if DEBUG
280-
let context = DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Value was nil")
281-
let error = DecodingError.valueNotFound(P.self, context)
282-
let decoder = try? superDecoder(forKey: key)
283-
decoder?.reportError(error)
284-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
285-
#else
286-
return DefaultCodable(wrappedValue: P.defaultValue)
287-
#endif
288-
}
289-
290-
// Try to decode the raw value
291-
do {
292-
let rawValue = try decode(P.DefaultValue.RawValue.self, forKey: key)
293-
294-
// Try to create the enum from raw value
295-
if let value = P.DefaultValue(rawValue: rawValue) {
296-
return DefaultCodable(wrappedValue: value)
297-
} else {
298-
#if DEBUG
299-
/// Unknown raw value
300-
let error = Self.createUnknownRawValueError(
301-
for: P.DefaultValue.self,
302-
rawValue: rawValue,
303-
codingPath: codingPath + [key],
304-
isFrozen: P.isFrozen
305-
)
306-
307-
let decoder = try? superDecoder(forKey: key)
308-
decoder?.reportError(error)
309-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
310-
#else
311-
return DefaultCodable(wrappedValue: P.defaultValue)
312-
#endif
313-
}
314-
} catch {
315-
#if DEBUG
316-
/// Decoding the raw value failed (e.g., type mismatch)
317-
let decoder = try? superDecoder(forKey: key)
318-
decoder?.reportError(error)
319-
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
320-
#else
321-
return DefaultCodable(wrappedValue: P.defaultValue)
322-
#endif
323-
}
324-
}
325-
326-
private static func createUnknownRawValueError<T: RawRepresentable>(
327-
for type: T.Type,
328-
rawValue: T.RawValue,
329-
codingPath: [CodingKey],
330-
isFrozen: Bool
331-
) -> Error {
332-
guard isFrozen else { return UnknownNovelValueError(novelValue: rawValue) }
333-
let context = DecodingError.Context(
334-
codingPath: codingPath,
335-
debugDescription: "Cannot initialize \(type) from invalid raw value \(rawValue)"
336-
)
337-
return DecodingError.dataCorrupted(context)
338-
}
339-
}

0 commit comments

Comments
 (0)