Skip to content

Commit 42a2cfb

Browse files
authored
Merge pull request #20 from tattn/feature/support-date-for-dictionarydecoder
Support Date for DictionaryDecoder
2 parents 2e1285c + 766f2f5 commit 42a2cfb

6 files changed

Lines changed: 190 additions & 7 deletions

File tree

.github/workflows/continuous-integration.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ jobs:
99
strategy:
1010
matrix:
1111
os:
12-
- ubuntu-latest
1312
- macOS-latest
13+
- ubuntu-latest
1414
runs-on: ${{ matrix.os }}
1515
steps:
16-
- uses: actions/checkout@v1
16+
- uses: actions/checkout@v2
1717
- run: rm .swift-version
1818
- name: Install Swift
19-
uses: YOCKOW/Action-setup-swift@master
19+
uses: YOCKOW/Action-setup-swift@v1
2020
with:
21-
swift-version: '5.1'
21+
swift-version: '5.9'
2222
- name: Test
2323
run: swift test --enable-test-discovery

Sources/DictionaryDecoder.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Foundation
1111

1212
open class DictionaryDecoder: Decoder {
1313
open var codingPath: [CodingKey]
14+
open var dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.deferredToDate
1415
open var userInfo: [CodingUserInfoKey: Any] = [:]
1516
var storage = Storage()
1617

@@ -49,10 +50,49 @@ open class DictionaryDecoder: Decoder {
4950
} catch {
5051
storage.push(container: value)
5152
defer { _ = storage.popContainer() }
53+
if type == Date.self {
54+
return try unwrapDate() as! T
55+
}
5256
return try T(from: self)
5357
}
5458
}
5559

60+
private func unwrapDate() throws -> Date {
61+
switch dateDecodingStrategy {
62+
case .deferredToDate:
63+
return try Date(from: self)
64+
65+
case .secondsSince1970:
66+
let container = SingleValueContainer(decoder: self)
67+
let double = try container.decode(Double.self)
68+
return Date(timeIntervalSince1970: double)
69+
70+
case .millisecondsSince1970:
71+
let container = SingleValueContainer(decoder: self)
72+
let double = try container.decode(Double.self)
73+
return Date(timeIntervalSince1970: double / 1000.0)
74+
75+
case .iso8601:
76+
let container = SingleValueContainer(decoder: self)
77+
let string = try container.decode(String.self)
78+
guard let date = _iso8601Formatter.date(from: string) else {
79+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
80+
}
81+
return date
82+
83+
case .formatted(let formatter):
84+
let container = SingleValueContainer(decoder: self)
85+
let string = try container.decode(String.self)
86+
guard let date = formatter.date(from: string) else {
87+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Date string does not match format expected by formatter."))
88+
}
89+
return date
90+
91+
case .custom(let closure):
92+
return try closure(self)
93+
}
94+
}
95+
5696
private func lastContainer<T>(forType type: T.Type) throws -> Any {
5797
guard let value = storage.last else {
5898
let description = "Expected \(type) but found nil value instead."

Sources/DictionaryEncoder.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010

1111
open class DictionaryEncoder: Encoder {
1212
open var codingPath: [CodingKey] = []
13+
open var dateEncodingStrategy = JSONEncoder.DateEncodingStrategy.deferredToDate
1314
open var userInfo: [CodingUserInfoKey: Any] = [:]
1415
private(set) var storage = Storage()
1516

@@ -28,8 +29,37 @@ open class DictionaryEncoder: Encoder {
2829
}
2930

3031
func box<T: Encodable>(_ value: T) throws -> Any {
31-
try value.encode(to: self)
32-
return storage.popContainer()
32+
switch value {
33+
case let date as Date:
34+
return try wrapDate(date)
35+
default:
36+
try value.encode(to: self)
37+
return storage.popContainer()
38+
}
39+
}
40+
41+
func wrapDate(_ date: Date) throws -> Any {
42+
switch dateEncodingStrategy {
43+
case .deferredToDate:
44+
try date.encode(to: self)
45+
return storage.popContainer()
46+
47+
case .secondsSince1970:
48+
return TimeInterval(date.timeIntervalSince1970.description) as Any
49+
50+
case .millisecondsSince1970:
51+
return TimeInterval((date.timeIntervalSince1970 * 1000).description) as Any
52+
53+
case .iso8601:
54+
return _iso8601Formatter.string(from: date)
55+
56+
case .formatted(let formatter):
57+
return formatter.string(from: date)
58+
59+
case .custom(let closure):
60+
try closure(date, self)
61+
return storage.popContainer()
62+
}
3363
}
3464
}
3565

Tests/DictionaryDecoderTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,50 @@ class DictionaryDecoderTests: XCTestCase {
6363
XCTAssertEqual(try decoder.decode(Model.self, from: ["int": 0, "string": "test"]), Model(int: 0, string: "test", double: nil))
6464
XCTAssertEqual(try decoder.decode(Model.self, from: ["double": 0.5, "string": "test"]), Model(int: nil, string: "test", double: 0.5))
6565
}
66+
67+
func testDate() throws {
68+
struct Model: Codable, Equatable {
69+
let date: Date
70+
let optionalDate: Date?
71+
}
72+
73+
let date = Date(timeIntervalSince1970: 1234567890)
74+
decoder.dateDecodingStrategy = .deferredToDate
75+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": date.timeIntervalSinceReferenceDate]), Model(date: date, optionalDate: nil))
76+
77+
decoder.dateDecodingStrategy = .secondsSince1970
78+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": date.timeIntervalSince1970]), Model(date: date, optionalDate: nil))
79+
80+
decoder.dateDecodingStrategy = .millisecondsSince1970
81+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": date.timeIntervalSince1970 * 1000]), Model(date: date, optionalDate: nil))
82+
83+
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) {
84+
decoder.dateDecodingStrategy = .iso8601
85+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": date.ISO8601Format()]), Model(date: date, optionalDate: nil))
86+
}
87+
88+
do {
89+
let dateFormatter = DateFormatter()
90+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
91+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
92+
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
93+
decoder.dateDecodingStrategy = .formatted(dateFormatter)
94+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": dateFormatter.string(from: date)]), Model(date: date, optionalDate: nil))
95+
}
96+
97+
decoder.dateDecodingStrategy = .custom { decoder in
98+
let contaienr = try decoder.singleValueContainer()
99+
return try contaienr.decode(Int.self) == 13 ? date : date.addingTimeInterval(.infinity)
100+
}
101+
XCTAssertEqual(try decoder.decode(Model.self, from: ["date": 13]), Model(date: date, optionalDate: nil))
102+
}
103+
}
104+
105+
#if os(Linux)
106+
private extension Date {
107+
func ISO8601Format() -> String {
108+
let formatter = ISO8601DateFormatter()
109+
return formatter.string(from: self)
110+
}
66111
}
112+
#endif

Tests/DictionaryEncoderTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,71 @@ class DictionaryEncoderTests: XCTestCase {
134134
let result = try encoder.encode(object)
135135
XCTAssertEqual(result as? [String: String], expected)
136136
}
137+
138+
func testEncodeDate() throws {
139+
struct Model: Codable {
140+
let date: Date
141+
let optionalDate: Date?
142+
}
143+
let date = Date(timeIntervalSince1970: 1234567890)
144+
let seeds: [(model: Model, count: Int)] = [
145+
(model: Model(date: date, optionalDate: nil), count: 1),
146+
(model: Model(date: date, optionalDate: date), count: 2),
147+
]
148+
for seed in seeds {
149+
encoder.dateEncodingStrategy = .deferredToDate
150+
var dictionary = try encoder.encode(seed.model)
151+
XCTAssertEqual(dictionary["date"] as? Double, seed.model.date.timeIntervalSinceReferenceDate)
152+
XCTAssertEqual(dictionary["optionalDate"] as? Double, seed.model.optionalDate?.timeIntervalSinceReferenceDate)
153+
XCTAssertEqual(dictionary.keys.count, seed.count)
154+
155+
encoder.dateEncodingStrategy = .secondsSince1970
156+
dictionary = try encoder.encode(seed.model)
157+
XCTAssertEqual(dictionary["date"] as? TimeInterval, seed.model.date.timeIntervalSince1970)
158+
XCTAssertEqual(dictionary["optionalDate"] as? TimeInterval, seed.model.optionalDate?.timeIntervalSince1970)
159+
XCTAssertEqual(dictionary.keys.count, seed.count)
160+
161+
encoder.dateEncodingStrategy = .millisecondsSince1970
162+
dictionary = try encoder.encode(seed.model)
163+
XCTAssertEqual(dictionary["date"] as? TimeInterval, seed.model.date.timeIntervalSince1970 * 1000)
164+
XCTAssertEqual(dictionary["optionalDate"] as? TimeInterval, seed.model.optionalDate.map { $0.timeIntervalSince1970 * 1000 })
165+
XCTAssertEqual(dictionary.keys.count, seed.count)
166+
167+
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) {
168+
encoder.dateEncodingStrategy = .iso8601
169+
dictionary = try encoder.encode(seed.model)
170+
XCTAssertEqual(dictionary["date"] as? String, seed.model.date.ISO8601Format())
171+
XCTAssertEqual(dictionary["optionalDate"] as? String, seed.model.optionalDate?.ISO8601Format())
172+
XCTAssertEqual(dictionary.keys.count, seed.count)
173+
}
174+
175+
let dateFormatter = DateFormatter()
176+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
177+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
178+
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
179+
encoder.dateEncodingStrategy = .formatted(dateFormatter)
180+
dictionary = try encoder.encode(seed.model)
181+
XCTAssertEqual(dictionary["date"] as? String, dateFormatter.string(from: seed.model.date))
182+
XCTAssertEqual(dictionary["optionalDate"] as? String, seed.model.optionalDate.map(dateFormatter.string))
183+
XCTAssertEqual(dictionary.keys.count, seed.count)
184+
185+
encoder.dateEncodingStrategy = .custom { date, encoder in
186+
var container = encoder.singleValueContainer()
187+
try container.encode(13)
188+
}
189+
dictionary = try encoder.encode(seed.model)
190+
XCTAssertEqual(dictionary["date"] as? Int, 13)
191+
XCTAssertEqual(dictionary["optionalDate"] as? Int, seed.model.optionalDate == nil ? nil : 13)
192+
XCTAssertEqual(dictionary.keys.count, seed.count)
193+
}
194+
}
195+
}
196+
197+
#if os(Linux)
198+
private extension Date {
199+
func ISO8601Format() -> String {
200+
let formatter = ISO8601DateFormatter()
201+
return formatter.string(from: self)
202+
}
137203
}
204+
#endif

Tests/FailableTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class FailableTests: XCTestCase {
4242

4343
func testFailableURL() {
4444
let json = """
45-
{"url": "https://foo.com", "url2": "invalid url string"}
45+
{"url": "https://foo.com", "url2": "a://invalid url string"}
4646
""".data(using: .utf8)!
4747

4848
struct Model: Codable {

0 commit comments

Comments
 (0)