Skip to content

Commit cd3b703

Browse files
committed
custom date coding via closures
1 parent 3d6ca2b commit cd3b703

File tree

3 files changed

+96
-27
lines changed

3 files changed

+96
-27
lines changed

Sources/KeyValueDecoder.swift

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,36 @@ public struct KeyValueDecoder: Sendable {
103103
/// Decodes dates by casting from Any.
104104
case date
105105

106-
/// Decodes dates from ISO8601 strings.
107-
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])
108-
109106
/// Decodes dates in terms of milliseconds since midnight UTC on January 1st, 1970.
110107
case millisecondsSince1970
111108

112109
/// Decodes dates in terms of seconds since midnight UTC on January 1st, 1970.
113110
case secondsSince1970
111+
112+
/// Decodes dates from Any using a closure
113+
case custom(@Sendable (Any) throws -> Date)
114+
115+
/// Decodes dates from ISO8601 strings.
116+
static func iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime]) -> Self {
117+
.custom {
118+
guard let string = $0 as? String else {
119+
throw Error("Expected String but found \(type(of: $0))")
120+
}
121+
let formatter = ISO8601DateFormatter()
122+
formatter.formatOptions = options
123+
guard let date = formatter.date(from: string) else {
124+
throw Error("Failed to decode Date from ISO8601 string \(string)")
125+
}
126+
return date
127+
}
128+
}
129+
}
130+
131+
struct Error: LocalizedError {
132+
var errorDescription: String?
133+
init(_ message: String) {
134+
self.errorDescription = message
135+
}
114136
}
115137
}
116138

@@ -359,19 +381,18 @@ private extension KeyValueDecoder {
359381
switch strategy.dates {
360382
case .date:
361383
return try getValue()
362-
case .iso8601(options: let options):
363-
let string = try decode(String.self)
364-
let formatter = ISO8601DateFormatter()
365-
formatter.formatOptions = options
366-
guard let date = formatter.date(from: string) else {
367-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Failed to decode Date from ISO8601 string \(string)"))
368-
}
369-
return date
370384
case .millisecondsSince1970:
371385
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)) / 1000)
372386

373387
case .secondsSince1970:
374388
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)))
389+
390+
case .custom(let transform):
391+
do {
392+
return try transform(self.value)
393+
} catch {
394+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: error.localizedDescription))
395+
}
375396
}
376397
}
377398

Sources/KeyValueEncoder.swift

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,23 @@ public struct KeyValueEncoder: Sendable {
7777
/// Encodes dates by directly casting to Any.
7878
case date
7979

80-
/// Encodes dates from ISO8601 strings.
81-
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])
82-
8380
/// Encodes dates to Int in terms of milliseconds since midnight UTC on January 1, 1970.
8481
case millisecondsSince1970
8582

8683
/// Encodes dates to Int in terms of seconds since midnight UTC on January 1, 1970.
8784
case secondsSince1970
85+
86+
/// Encodes dates to Any using a closure
87+
case custom(@Sendable (Date) throws -> Any)
88+
89+
/// Encodes dates to ISO8601 strings.
90+
static func iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime]) -> Self {
91+
.custom {
92+
let formatter = ISO8601DateFormatter()
93+
formatter.formatOptions = options
94+
return formatter.string(from: $0)
95+
}
96+
}
8897
}
8998
}
9099

@@ -238,7 +247,7 @@ private extension KeyValueEncoder {
238247
}
239248

240249
func encodeToValue<T>(_ value: T) throws -> EncodedValue where T: Encodable {
241-
guard let encoded = EncodedValue.makeValue(for: value, using: strategy) else {
250+
guard let encoded = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) else {
242251
try value.encode(to: self)
243252
return try getEncodedValue()
244253
}
@@ -338,7 +347,7 @@ private extension KeyValueEncoder {
338347
}
339348

340349
func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
341-
if let val = EncodedValue.makeValue(for: value, using: strategy) {
350+
if let val = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) {
342351
setValue(val, forKey: key)
343352
return
344353
}
@@ -468,7 +477,7 @@ private extension KeyValueEncoder {
468477
}
469478

470479
func encode<T: Encodable>(_ value: T) throws {
471-
if let val = EncodedValue.makeValue(for: value, using: strategy) {
480+
if let val = try EncodedValue.makeValue(for: value, at: codingPath.appending(index: count), using: strategy) {
472481
appendValue(val)
473482
return
474483
}
@@ -587,7 +596,7 @@ private extension KeyValueEncoder {
587596
}
588597

589598
func encode<T>(_ value: T) throws where T: Encodable {
590-
if let encoded = EncodedValue.makeValue(for: value, using: strategy) {
599+
if let encoded = try EncodedValue.makeValue(for: value, at: codingPath, using: strategy) {
591600
self.value = encoded
592601
return
593602
}
@@ -708,11 +717,25 @@ struct AnyCodingKey: CodingKey {
708717

709718
extension KeyValueEncoder.EncodedValue {
710719

711-
static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) -> Self? {
720+
static func makeValue(for value: Any, at codingPath: [any CodingKey], using strategy: KeyValueEncoder.EncodingStrategy) throws -> Self? {
721+
do {
722+
return try makeValue(for: value, using: strategy)
723+
} catch {
724+
let valueDescription = strategy.optionals.isNull(value) ? "nil" : String(describing: type(of: value))
725+
let context = EncodingError.Context(
726+
codingPath: codingPath,
727+
debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()) cannot be encoded. \(error.localizedDescription)",
728+
underlyingError: error
729+
)
730+
throw EncodingError.invalidValue(value, context)
731+
}
732+
}
733+
734+
static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) throws -> Self? {
712735
if let dataValue = value as? Data {
713736
return .value(dataValue)
714737
} else if let dateValue = value as? Date {
715-
return makeValue(for: dateValue, using: strategy.dates)
738+
return try makeValue(for: dateValue, using: strategy.dates)
716739
} else if let urlValue = value as? URL {
717740
return .value(urlValue)
718741
} else if let decimalValue = value as? Decimal {
@@ -722,14 +745,12 @@ extension KeyValueEncoder.EncodedValue {
722745
}
723746
}
724747

725-
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) -> Self? {
748+
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) throws -> Self? {
726749
switch strategy {
727750
case .date:
728751
return .value(date)
729-
case .iso8601(options: let options):
730-
let f = ISO8601DateFormatter()
731-
f.formatOptions = options
732-
return .value(f.string(from: date))
752+
case .custom(let transform):
753+
return try .value(transform(date))
733754
case .millisecondsSince1970:
734755
return .value(Int(date.timeIntervalSince1970 * 1000))
735756
case .secondsSince1970:

Tests/KeyValueEncoderTests.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,17 @@ struct KeyValueEncodedTests {
695695
#expect(
696696
try encoder.encode(referenceDate) as? Int == 978307200
697697
)
698+
699+
encoder.dateEncodingStrategy = .custom { _ in throw KeyValueDecoder.Error("🐟") }
700+
var error = #expect(throws: EncodingError.self) {
701+
try encoder.encode(referenceDate)
702+
}
703+
#expect(error?.context?.debugDescription == "Date at SELF cannot be encoded. 🐟")
704+
705+
error = #expect(throws: EncodingError.self) {
706+
try encoder.encode(["calendar": [Date?.none, referenceDate]])
707+
}
708+
#expect(error?.context?.debugDescription == "Optional<Date> at SELF.calendar[1] cannot be encoded. 🐟")
698709
}
699710

700711
@Test
@@ -708,7 +719,7 @@ struct KeyValueEncodedTests {
708719
}
709720

710721
@Test
711-
func aa() {
722+
func isOptionalNone() {
712723
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int?.none as Any))
713724
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int??.none as Any))
714725
}
@@ -831,7 +842,11 @@ extension KeyValueEncoder.EncodedValue {
831842

832843
private extension KeyValueEncoder.EncodedValue {
833844
static func isSupportedValue(_ value: Any) -> Bool {
834-
Self.makeValue(for: value, using: .default) != nil
845+
do {
846+
return try Self.makeValue(for: value, using: .default) != nil
847+
} catch {
848+
return false
849+
}
835850
}
836851
}
837852

@@ -869,4 +884,16 @@ private struct Null: Encodable {
869884
try container.encodeNil()
870885
}
871886
}
887+
888+
private extension EncodingError {
889+
890+
var context: Context? {
891+
switch self {
892+
case .invalidValue(_, let context):
893+
return context
894+
default:
895+
return nil
896+
}
897+
}
898+
}
872899
#endif

0 commit comments

Comments
 (0)