Skip to content

Commit 21af7d1

Browse files
committed
DateEncodingStrategy
1 parent f2058a2 commit 21af7d1

File tree

3 files changed

+141
-57
lines changed

3 files changed

+141
-57
lines changed

Sources/KeyValueEncoder.swift

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ public struct KeyValueEncoder: Sendable {
3737
/// Contextual user-provided information for use during encoding.
3838
public var userInfo: [CodingUserInfoKey: any Sendable]
3939

40-
/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
41-
public var nilEncodingStrategy: NilEncodingStrategy = .default
40+
/// The strategy to use for encoding Date types.
41+
public var dateEncodingStrategy: DateEncodingStrategy = .date
4242

4343
/// The strategy to use for encoding each types keys.
4444
public var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
4545

46+
/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
47+
public var nilEncodingStrategy: NilEncodingStrategy = .default
48+
49+
4650
/// Initializes `self` with default strategies.
4751
public init () {
4852
self.userInfo = [:]
@@ -67,6 +71,21 @@ public struct KeyValueEncoder: Sendable {
6771
/// A key encoding strategy that doesn’t change key names during encoding.
6872
case useDefaultKeys
6973
}
74+
75+
public enum DateEncodingStrategy: Sendable {
76+
77+
/// Encodes dates by directly casting to Any.
78+
case date
79+
80+
/// Encodes dates from ISO8601 strings.
81+
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])
82+
83+
/// Encodes dates to Int in terms of milliseconds since midnight UTC on January 1, 1970.
84+
case millisecondsSince1970
85+
86+
/// Encodes dates to Int in terms of seconds since midnight UTC on January 1, 1970.
87+
case secondsSince1970
88+
}
7089
}
7190

7291
/// Strategy used to encode and decode nil values.
@@ -96,6 +115,12 @@ extension KeyValueEncoder: TopLevelEncoder {
96115

97116
extension KeyValueEncoder {
98117

118+
struct EncodingStrategy {
119+
var optionals: NilEncodingStrategy
120+
var keys: KeyEncodingStrategy
121+
var dates: DateEncodingStrategy
122+
}
123+
99124
static func makePlistCompatible() -> KeyValueEncoder {
100125
var encoder = KeyValueEncoder()
101126
encoder.nilEncodingStrategy = .stringNull
@@ -161,15 +186,11 @@ private extension KeyValueEncoder.NilEncodingStrategy {
161186

162187
private extension KeyValueEncoder {
163188

164-
struct EncodingStrategy {
165-
var optionals: NilEncodingStrategy
166-
var keys: KeyEncodingStrategy
167-
}
168-
169189
var strategy: EncodingStrategy {
170190
EncodingStrategy(
171191
optionals: nilEncodingStrategy,
172-
keys: keyEncodingStrategy
192+
keys: keyEncodingStrategy,
193+
dates: dateEncodingStrategy
173194
)
174195
}
175196

@@ -217,7 +238,7 @@ private extension KeyValueEncoder {
217238
}
218239

219240
func encodeToValue<T>(_ value: T) throws -> EncodedValue where T: Encodable {
220-
guard let encoded = EncodedValue(value) else {
241+
guard let encoded = EncodedValue.makeValue(for: value, using: strategy) else {
221242
try value.encode(to: self)
222243
return try getEncodedValue()
223244
}
@@ -317,7 +338,7 @@ private extension KeyValueEncoder {
317338
}
318339

319340
func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
320-
if let val = EncodedValue(value) {
341+
if let val = EncodedValue.makeValue(for: value, using: strategy) {
321342
setValue(val, forKey: key)
322343
return
323344
}
@@ -447,7 +468,7 @@ private extension KeyValueEncoder {
447468
}
448469

449470
func encode<T: Encodable>(_ value: T) throws {
450-
if let val = EncodedValue(value) {
471+
if let val = EncodedValue.makeValue(for: value, using: strategy) {
451472
appendValue(val)
452473
return
453474
}
@@ -566,7 +587,7 @@ private extension KeyValueEncoder {
566587
}
567588

568589
func encode<T>(_ value: T) throws where T: Encodable {
569-
if let encoded = EncodedValue(value) {
590+
if let encoded = EncodedValue.makeValue(for: value, using: strategy) {
570591
self.value = encoded
571592
return
572593
}
@@ -687,20 +708,32 @@ struct AnyCodingKey: CodingKey {
687708

688709
extension KeyValueEncoder.EncodedValue {
689710

690-
static func isSupportedValue(_ value: Any) -> Bool {
691-
switch value {
692-
case is Data: return true
693-
case is Date: return true
694-
case is URL: return true
695-
case is Decimal: return true
696-
default: return false
711+
static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) -> Self? {
712+
if let dataValue = value as? Data {
713+
return .value(dataValue)
714+
} else if let dateValue = value as? Date {
715+
return makeValue(for: dateValue, using: strategy.dates)
716+
} else if let urlValue = value as? URL {
717+
return .value(urlValue)
718+
} else if let decimalValue = value as? Decimal {
719+
return .value(decimalValue)
720+
} else {
721+
return nil
697722
}
698723
}
699724

700-
init?(_ value: Any) {
701-
guard Self.isSupportedValue(value) else {
702-
return nil
725+
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) -> Self? {
726+
switch strategy {
727+
case .date:
728+
return .value(date)
729+
case .iso8601(options: let options):
730+
let f = ISO8601DateFormatter()
731+
f.formatOptions = options
732+
return .value(f.string(from: date))
733+
case .millisecondsSince1970:
734+
return .value(Int(date.timeIntervalSince1970 * 1000))
735+
case .secondsSince1970:
736+
return .value(Int(date.timeIntervalSince1970))
703737
}
704-
self = .value(value)
705738
}
706739
}

Tests/KeyValueEncoderTests.swift

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ struct KeyValueEncodedTests {
8787
#expect(
8888
try KeyValueEncoder.encodeSingleValue {
8989
try $0.encode(URL(string: "fish.com")!)
90-
} == EncodedValue(URL(string: "fish.com")!)
90+
} == .value(URL(string: "fish.com")!)
9191
)
9292
}
9393

@@ -202,10 +202,10 @@ struct KeyValueEncodedTests {
202202
@Test
203203
func encodes() throws {
204204
let node = Node(id: 1,
205-
name: "root",
206-
descendents: [Node(id: 2), Node(id: 3)],
207-
related: ["left": Node(id: 4, descendents: [Node(id: 5)]),
208-
"right": Node(id: 6)]
205+
name: "root",
206+
descendents: [Node(id: 2), Node(id: 3)],
207+
related: ["left": Node(id: 4, descendents: [Node(id: 5)]),
208+
"right": Node(id: 6)]
209209
)
210210

211211
#expect(
@@ -245,9 +245,9 @@ struct KeyValueEncodedTests {
245245

246246
#expect(
247247
try KeyValueEncoder().encode(real) as? NSDictionary == [
248-
"tBool": true,
249-
"tArray": [["tBool": false]]
250-
]
248+
"tBool": true,
249+
"tArray": [["tBool": false]]
250+
]
251251
)
252252
}
253253

@@ -260,9 +260,9 @@ struct KeyValueEncodedTests {
260260

261261
#expect(
262262
try KeyValueEncoder().encode(real) as? NSDictionary == [
263-
"tDouble": 20,
264-
"tFloat": -10
265-
]
263+
"tDouble": 20,
264+
"tFloat": -10
265+
]
266266
)
267267
}
268268

@@ -274,8 +274,8 @@ struct KeyValueEncodedTests {
274274

275275
#expect(
276276
try KeyValueEncoder().encode(urls) as? NSDictionary == [
277-
"tURL": URL(string: "fish.com")!
278-
]
277+
"tURL": URL(string: "fish.com")!
278+
]
279279
)
280280
}
281281

@@ -293,14 +293,14 @@ struct KeyValueEncodedTests {
293293

294294
#expect(
295295
try KeyValueEncoder().encode(ints) as? NSDictionary == [
296-
"tInt": 10,
297-
"tInt8": -20,
298-
"tInt16": 30,
299-
"tInt32": -40,
300-
"tInt64": Int64.max,
301-
"tArray": [["tInt": -1], ["tInt": -2]],
302-
"tDictionary": ["rel": ["tInt": -3]]
303-
]
296+
"tInt": 10,
297+
"tInt8": -20,
298+
"tInt16": 30,
299+
"tInt32": -40,
300+
"tInt64": Int64.max,
301+
"tArray": [["tInt": -1], ["tInt": -2]],
302+
"tDictionary": ["rel": ["tInt": -3]]
303+
]
304304
)
305305
}
306306

@@ -318,14 +318,14 @@ struct KeyValueEncodedTests {
318318

319319
#expect(
320320
try KeyValueEncoder().encode(uints) as? NSDictionary == [
321-
"tUInt": 10,
322-
"tUInt8": 20,
323-
"tUInt16": 30,
324-
"tUInt32": 40,
325-
"tUInt64": UInt64.max,
326-
"tArray": [["tUInt": 50], ["tUInt": 60]],
327-
"tDictionary": ["rel": ["tUInt": 70]]
328-
]
321+
"tUInt": 10,
322+
"tUInt8": 20,
323+
"tUInt16": 30,
324+
"tUInt32": 40,
325+
"tUInt64": UInt64.max,
326+
"tArray": [["tUInt": 50], ["tUInt": 60]],
327+
"tDictionary": ["rel": ["tUInt": 70]]
328+
]
329329
)
330330
}
331331

@@ -654,7 +654,7 @@ struct KeyValueEncodedTests {
654654
)
655655
}
656656

657-
#if !os(WASI)
657+
#if !os(WASI)
658658
@Test
659659
func plistCompatibleEncoder() throws {
660660
let keyValueAny = try KeyValueEncoder.makePlistCompatible().encode([1, 2, Int?.none, 4])
@@ -664,13 +664,36 @@ struct KeyValueEncodedTests {
664664
]
665665
)
666666
}
667-
#endif
667+
#endif
668668

669669
@Test
670670
func encoder_Encodes_Dates() throws {
671-
let date = Date()
671+
var encoder = KeyValueEncoder()
672+
let referenceDate = Date(timeIntervalSinceReferenceDate: 0)
673+
674+
encoder.dateEncodingStrategy = .date
675+
#expect(
676+
try encoder.encode(referenceDate) as? Date == referenceDate
677+
)
678+
679+
encoder.dateEncodingStrategy = .iso8601()
680+
#expect(
681+
try encoder.encode(referenceDate) as? String == "2001-01-01T00:00:00Z"
682+
)
683+
684+
encoder.dateEncodingStrategy = .iso8601(options: [.withInternetDateTime, .withFractionalSeconds])
672685
#expect(
673-
try KeyValueEncoder().encode(date) as? Date == date
686+
try encoder.encode(referenceDate) as? String == "2001-01-01T00:00:00.000Z"
687+
)
688+
689+
encoder.dateEncodingStrategy = .millisecondsSince1970
690+
#expect(
691+
try encoder.encode(referenceDate) as? Int == 978307200000
692+
)
693+
694+
encoder.dateEncodingStrategy = .secondsSince1970
695+
#expect(
696+
try encoder.encode(referenceDate) as? Int == 978307200
674697
)
675698
}
676699

@@ -806,6 +829,20 @@ extension KeyValueEncoder.EncodedValue {
806829
}
807830
}
808831

832+
private extension KeyValueEncoder.EncodedValue {
833+
static func isSupportedValue(_ value: Any) -> Bool {
834+
Self.makeValue(for: value, using: .default) != nil
835+
}
836+
}
837+
838+
private extension KeyValueEncoder.EncodingStrategy {
839+
static let `default` = Self(
840+
optionals: .default,
841+
keys: .useDefaultKeys,
842+
dates: .date
843+
)
844+
}
845+
809846
#if !os(WASI)
810847
private extension PropertyListDecoder {
811848
static func decodeAny<T: Decodable>(_ type: T.Type, from value: Any?) throws -> T {

Tests/KeyValueEncoderXCTests.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ final class KeyValueEncodedXCTests: XCTestCase {
8787
try KeyValueEncoder.encodeSingleValue {
8888
try $0.encode(URL(string: "fish.com")!)
8989
},
90-
EncodedValue(URL(string: "fish.com")!)
90+
.value(URL(string: "fish.com")!)
9191
)
9292
}
9393

@@ -767,6 +767,20 @@ extension KeyValueEncoder.EncodedValue {
767767
}
768768
}
769769

770+
private extension KeyValueEncoder.EncodedValue {
771+
static func isSupportedValue(_ value: Any) -> Bool {
772+
Self.makeValue(for: value, using: .default) != nil
773+
}
774+
}
775+
776+
private extension KeyValueEncoder.EncodingStrategy {
777+
static let `default` = Self(
778+
optionals: .default,
779+
keys: .useDefaultKeys,
780+
dates: .date
781+
)
782+
}
783+
770784
#if !os(WASI)
771785
private extension PropertyListDecoder {
772786
static func decodeAny<T: Decodable>(_ type: T.Type, from value: Any?) throws -> T {

0 commit comments

Comments
 (0)