Skip to content

Commit 7ba43c0

Browse files
authored
Merge pull request #15 from swhitty/custom-date-encoding
custom date coding via closures
2 parents 3d6ca2b + 9c5fc8e commit 7ba43c0

File tree

5 files changed

+103
-32
lines changed

5 files changed

+103
-32
lines changed

.github/workflows/build.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ jobs:
1313
steps:
1414
- name: Checkout
1515
uses: actions/checkout@v4
16-
- name: 🔍 Xcode Select
17-
run: |
18-
XCODE_PATH=`mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode' && kMDItemVersion == '16.*'" -onlyin /Applications | head -1`
19-
echo "DEVELOPER_DIR=$XCODE_PATH/Contents/Developer" >> $GITHUB_ENV
2016
- name: Version
2117
run: swift --version
2218
- name: Build

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: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,19 @@ struct KeyValueEncodedTests {
695695
#expect(
696696
try encoder.encode(referenceDate) as? Int == 978307200
697697
)
698+
699+
#if compiler(>=6.1)
700+
encoder.dateEncodingStrategy = .custom { _ in throw KeyValueDecoder.Error("🐟") }
701+
var error = #expect(throws: EncodingError.self) {
702+
try encoder.encode(referenceDate)
703+
}
704+
#expect(error?.context?.debugDescription == "Date at SELF cannot be encoded. 🐟")
705+
706+
error = #expect(throws: EncodingError.self) {
707+
try encoder.encode(["calendar": [Date?.none, referenceDate]])
708+
}
709+
#expect(error?.context?.debugDescription == "Optional<Date> at SELF.calendar[1] cannot be encoded. 🐟")
710+
#endif
698711
}
699712

700713
@Test
@@ -708,7 +721,7 @@ struct KeyValueEncodedTests {
708721
}
709722

710723
@Test
711-
func aa() {
724+
func isOptionalNone() {
712725
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int?.none as Any))
713726
#expect(KeyValueEncoder.NilEncodingStrategy.isOptionalNone(Int??.none as Any))
714727
}
@@ -831,7 +844,11 @@ extension KeyValueEncoder.EncodedValue {
831844

832845
private extension KeyValueEncoder.EncodedValue {
833846
static func isSupportedValue(_ value: Any) -> Bool {
834-
Self.makeValue(for: value, using: .default) != nil
847+
do {
848+
return try Self.makeValue(for: value, using: .default) != nil
849+
} catch {
850+
return false
851+
}
835852
}
836853
}
837854

@@ -869,4 +886,16 @@ private struct Null: Encodable {
869886
try container.encodeNil()
870887
}
871888
}
889+
890+
private extension EncodingError {
891+
892+
var context: Context? {
893+
switch self {
894+
case .invalidValue(_, let context):
895+
return context
896+
default:
897+
return nil
898+
}
899+
}
900+
}
872901
#endif

Tests/KeyValueEncoderXCTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,11 @@ extension KeyValueEncoder.EncodedValue {
769769

770770
private extension KeyValueEncoder.EncodedValue {
771771
static func isSupportedValue(_ value: Any) -> Bool {
772-
Self.makeValue(for: value, using: .default) != nil
772+
do {
773+
return try Self.makeValue(for: value, using: .default) != nil
774+
} catch {
775+
return false
776+
}
773777
}
774778
}
775779

0 commit comments

Comments
 (0)