Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,15 @@ extension OptionalDateValue: Decodable where Formatter.RawValue: Decodable {
#endif
throw error
}

} catch DecodingError.keyNotFound {
self.wrappedValue = nil
self.outcome = .keyNotFound

} catch DecodingError.valueNotFound(let rawType, _) where rawType == Formatter.RawValue.self {
self.wrappedValue = nil
self.outcome = .valueWasNil

} catch {
#if DEBUG
decoder.reportError(error)
Expand Down Expand Up @@ -96,20 +102,38 @@ extension KeyedDecodingContainer {
_ type: OptionalDateValue<T>.Type,
forKey key: Self.Key
) throws -> OptionalDateValue<T> where T.RawValue: Decodable {
try decodeIfPresent(type, forKey: key) ?? OptionalDateValue<T>(wrappedValue: nil)
// Check if the key exists
guard contains(key) else {
return OptionalDateValue<T>(wrappedValue: nil, outcome: .keyNotFound)
}

// Check if the value is null
if try decodeNil(forKey: key) {
return OptionalDateValue<T>(wrappedValue: nil, outcome: .valueWasNil)
}

// Try to decode using the generic approach
let value = try decodeIfPresent(type, forKey: key)
return value ?? OptionalDateValue<T>(wrappedValue: nil, outcome: .keyNotFound)
}

public func decodeIfPresent<T>(
_ type: OptionalDateValue<T>.Type,
forKey key: Self.Key
) throws -> OptionalDateValue<T> where T.RawValue == String {
let stringOptionalValue = try decodeIfPresent(String.self, forKey: key)
forKey key: Self.Key,
) throws -> OptionalDateValue<T>? where T.RawValue == String {
// Check if the key exists
guard contains(key) else {
return nil
}

guard let stringValue = stringOptionalValue else {
return .init(wrappedValue: nil)
// Check if the value is null
if try decodeNil(forKey: key) {
return OptionalDateValue<T>(wrappedValue: nil, outcome: .valueWasNil)
}

// Try to decode the string value
let stringValue = try decode(String.self, forKey: key)
let dateValue = try T.decode(stringValue)
return .init(wrappedValue: dateValue)
return OptionalDateValue<T>(wrappedValue: dateValue, outcome: .decodedSuccessfully)
}
}
227 changes: 0 additions & 227 deletions Sources/KarrotCodableKit/BetterCodable/Defaults/DefaultCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,230 +110,3 @@ extension DefaultCodable: Hashable where Default.DefaultValue: Hashable {
}

extension DefaultCodable: Sendable where Default.DefaultValue: Sendable {}

// MARK: - KeyedDecodingContainer

public protocol BoolCodableStrategy: DefaultCodableStrategy where DefaultValue == Bool {}

extension KeyedDecodingContainer {
/// Default implementation of decoding a DefaultCodable
///
/// Decodes successfully if key is available if not fallback to the default value provided.
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
// Check if key exists
if !contains(key) {
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Key not found but property is non-optional"
)
let error = DecodingError.keyNotFound(key, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

// Check for nil
if (try? decodeNil(forKey: key)) == true {
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Value was nil but property is non-optional"
)
let error = DecodingError.valueNotFound(P.DefaultValue.self, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

// Try to decode normally
if let value = try decodeIfPresent(DefaultCodable<P>.self, forKey: key) {
return value
} else {
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Key not found but property is non-optional"
)
let error = DecodingError.keyNotFound(key, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}
}

/// Default implementation of decoding a `DefaultCodable` where its strategy is a `BoolCodableStrategy`.
///
/// Tries to initially Decode a `Bool` if available, otherwise tries to decode it as an `Int` or `String`
/// when there is a `typeMismatch` decoding error. This preserves the actual value of the `Bool` in which
/// the data provider might be sending the value as different types. If everything fails defaults to
/// the `defaultValue` provided by the strategy.
public func decode<P: BoolCodableStrategy>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P> {
// Check if key exists
if !contains(key) {
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Key not found but property is non-optional"
)
let error = DecodingError.keyNotFound(key, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

// Check for nil first
if (try? decodeNil(forKey: key)) == true {
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Value was nil but property is non-optional"
)
let error = DecodingError.valueNotFound(Bool.self, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

do {
let value = try decode(Bool.self, forKey: key)
return DefaultCodable(wrappedValue: value)
} catch {
guard
let decodingError = error as? DecodingError,
case .typeMismatch = decodingError
else {
// Report error and use default
#if DEBUG
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}
if
let intValue = try? decodeIfPresent(Int.self, forKey: key),
let bool = Bool(exactly: NSNumber(value: intValue))
{
return DefaultCodable(wrappedValue: bool)
} else if
let stringValue = try? decodeIfPresent(String.self, forKey: key),
let bool = Bool(stringValue)
{
return DefaultCodable(wrappedValue: bool)
} else {
// Type mismatch - report error
#if DEBUG
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(decodingError)
return DefaultCodable(
wrappedValue: P.defaultValue,
outcome: .recoveredFrom(decodingError, wasReported: decoder != nil)
)
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}
}
}

/// Decodes a DefaultCodable where the strategy's DefaultValue is RawRepresentable
///
/// This method provides special handling for RawRepresentable types:
/// - If `isFrozen` is false (default), unknown raw values result in UnknownNovelValueError and use the default value
/// - If `isFrozen` is true, unknown raw values result in DecodingError and use the default value
public func decode<P>(_: DefaultCodable<P>.Type, forKey key: Key) throws -> DefaultCodable<P>
where P.DefaultValue: RawRepresentable, P.DefaultValue.RawValue: Decodable
{
// Check if key exists
if !contains(key) {
#if DEBUG
let context = DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Key not found")
let error = DecodingError.keyNotFound(key, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

// Check for nil
if (try? decodeNil(forKey: key)) == true {
#if DEBUG
let context = DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Value was nil")
let error = DecodingError.valueNotFound(P.self, context)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}

// Try to decode the raw value
do {
let rawValue = try decode(P.DefaultValue.RawValue.self, forKey: key)

// Try to create the enum from raw value
if let value = P.DefaultValue(rawValue: rawValue) {
return DefaultCodable(wrappedValue: value)
} else {
#if DEBUG
/// Unknown raw value
let error = Self.createUnknownRawValueError(
for: P.DefaultValue.self,
rawValue: rawValue,
codingPath: codingPath + [key],
isFrozen: P.isFrozen
)

let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}
} catch {
#if DEBUG
/// Decoding the raw value failed (e.g., type mismatch)
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return DefaultCodable(wrappedValue: P.defaultValue, outcome: .recoveredFrom(error, wasReported: decoder != nil))
#else
return DefaultCodable(wrappedValue: P.defaultValue)
#endif
}
}

private static func createUnknownRawValueError<T: RawRepresentable>(
for type: T.Type,
rawValue: T.RawValue,
codingPath: [CodingKey],
isFrozen: Bool
) -> Error {
guard isFrozen else { return UnknownNovelValueError(novelValue: rawValue) }
let context = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Cannot initialize \(type) from invalid raw value \(rawValue)"
)
return DecodingError.dataCorrupted(context)
}
}
Loading