Skip to content

Commit 2fa2929

Browse files
authored
Fix user-specified Date objects not being output in ISO8601 format (#273)
* Fix user-specified dates not being output in ISO8601 * Who knew `now` was such a recent swift addition.
1 parent 86aeff4 commit 2fa2929

File tree

8 files changed

+78
-18
lines changed

8 files changed

+78
-18
lines changed

Sources/Segment/ObjC/ObjCAnalytics.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ extension ObjCAnalytics {
164164
var result: [String: Any]? = nil
165165
if let system: System = analytics.store.currentState() {
166166
do {
167-
let encoder = JSONEncoder()
167+
let encoder = JSONEncoder.default
168168
let json = try encoder.encode(system.settings)
169169
if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] {
170170
result = r

Sources/Segment/ObjC/ObjCConfiguration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public class ObjCConfiguration: NSObject {
7575
get {
7676
var result = [String: Any]()
7777
do {
78-
let encoder = JSONEncoder()
78+
let encoder = JSONEncoder.default
7979
let json = try encoder.encode(configuration.values.defaultSettings)
8080
if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] {
8181
result = r
@@ -89,7 +89,7 @@ public class ObjCConfiguration: NSObject {
8989
set(value) {
9090
do {
9191
let json = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
92-
let decoder = JSONDecoder()
92+
let decoder = JSONDecoder.default
9393
let settings = try decoder.decode(Settings.self, from: json)
9494
configuration.defaultSettings(settings)
9595
} catch {

Sources/Segment/Settings.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public struct Settings: Codable {
4646
static public func load(from url: URL?) -> Settings? {
4747
guard let url = url else { return nil }
4848
guard let data = try? Data(contentsOf: url) else { return nil }
49-
let settings = try? JSONDecoder().decode(Settings.self, from: data)
49+
let settings = try? JSONDecoder.default.decode(Settings.self, from: data)
5050
return settings
5151
}
5252

@@ -80,7 +80,7 @@ public struct Settings: Codable {
8080
var result: T? = nil
8181
guard let settings = integrations?.dictionaryValue else { return nil }
8282
if let dict = settings[key], let jsonData = try? JSONSerialization.data(withJSONObject: dict) {
83-
result = try? JSONDecoder().decode(T.self, from: jsonData)
83+
result = try? JSONDecoder.default.decode(T.self, from: jsonData)
8484
}
8585
return result
8686
}

Sources/Segment/Utilities/HTTPClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public class HTTPClient {
118118
}
119119

120120
do {
121-
let responseJSON = try JSONDecoder().decode(Settings.self, from: data)
121+
let responseJSON = try JSONDecoder.default.decode(Settings.self, from: data)
122122
completion(true, responseJSON)
123123
} catch {
124124
self?.analytics?.reportInternalError(AnalyticsError.jsonUnableToDeserialize(error))

Sources/Segment/Utilities/JSON.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public enum JSON: Equatable {
3535

3636
// For Value types
3737
public init<T: Codable>(with value: T) throws {
38-
let encoder = JSONEncoder()
38+
let encoder = JSONEncoder.default
3939
let json = try encoder.encode(value)
4040
let output = try JSONSerialization.jsonObject(with: json)
4141
try self.init(output)
@@ -58,6 +58,8 @@ public enum JSON: Equatable {
5858
// handle swift types
5959
case Optional<Any>.none:
6060
self = .null
61+
case let date as Date:
62+
self = .string(date.iso8601())
6163
case let url as URL:
6264
self = .string(url.absoluteString)
6365
case let string as String:
@@ -134,7 +136,7 @@ extension Encodable {
134136
public func toString(pretty: Bool) -> String {
135137
var returnString = ""
136138
do {
137-
let encoder = JSONEncoder()
139+
let encoder = JSONEncoder.default
138140
if pretty {
139141
encoder.outputFormatting = .prettyPrinted
140142
}
@@ -182,7 +184,11 @@ extension JSON {
182184
public func codableValue<T: Codable>() -> T? {
183185
var result: T? = nil
184186
if let dict = dictionaryValue, let jsonData = try? JSONSerialization.data(withJSONObject: dict) {
185-
result = try? JSONDecoder().decode(T.self, from: jsonData)
187+
do {
188+
result = try JSONDecoder.default.decode(T.self, from: jsonData)
189+
} catch {
190+
print(error)
191+
}
186192
}
187193
return result
188194
}
@@ -402,7 +408,7 @@ extension JSON {
402408
if let v = value as? [String: Any] {
403409
if let jsonData = try? JSONSerialization.data(withJSONObject: v) {
404410
do {
405-
result = try JSONDecoder().decode(T.self, from: jsonData)
411+
result = try JSONDecoder.default.decode(T.self, from: jsonData)
406412
} catch {
407413
Analytics.segmentLog(message: "Unable to decode object (\(keyPath)) to a Codable: \(error)", kind: .error)
408414
}

Sources/Segment/Utilities/iso8601.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import Foundation
99

1010
enum SegmentISO8601DateFormatter {
11-
1211
static let shared: ISO8601DateFormatter = {
1312
let formatter = ISO8601DateFormatter()
1413
formatter.formatOptions.update(with: .withFractionalSeconds)
@@ -28,3 +27,30 @@ internal extension String {
2827
return SegmentISO8601DateFormatter.shared.date(from: self)
2928
}
3029
}
30+
31+
extension DateFormatter {
32+
static let iso8601: DateFormatter = {
33+
let formatter = DateFormatter()
34+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
35+
formatter.calendar = Calendar(identifier: .iso8601)
36+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
37+
formatter.locale = Locale(identifier: "en_US_POSIX")
38+
return formatter
39+
}()
40+
}
41+
42+
extension JSONDecoder {
43+
static var `default`: JSONDecoder {
44+
let d = JSONDecoder()
45+
d.dateDecodingStrategy = .formatted(DateFormatter.iso8601)
46+
return d
47+
}
48+
}
49+
50+
extension JSONEncoder {
51+
static var `default`: JSONEncoder {
52+
let e = JSONEncoder()
53+
e.dateEncodingStrategy = .formatted(DateFormatter.iso8601)
54+
return e
55+
}
56+
}

Tests/Segment-Tests/Analytics_Tests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ final class Analytics_Tests: XCTestCase {
692692

693693
}
694694

695-
func testAsyncOperatingMode() {
695+
func testAsyncOperatingMode() throws {
696696
// Use a specific writekey to this test so we do not collide with other cached items.
697697
let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_asyncMode")
698698
.flushInterval(9999)
@@ -721,7 +721,7 @@ final class Analytics_Tests: XCTestCase {
721721
XCTAssertEqual(analytics.pendingUploads!.count, 0)
722722
}
723723

724-
func testSyncOperatingMode() {
724+
func testSyncOperatingMode() throws {
725725
// Use a specific writekey to this test so we do not collide with other cached items.
726726
let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_syncMode")
727727
.flushInterval(9999)
@@ -755,7 +755,7 @@ final class Analytics_Tests: XCTestCase {
755755
XCTAssertEqual(analytics.pendingUploads!.count, 0)
756756
}
757757

758-
func testFindAll() {
758+
func testFindAll() throws {
759759
let analytics = Analytics(configuration: Configuration(writeKey: "testFindAll")
760760
.flushInterval(9999)
761761
.flushAt(9999)

Tests/Segment-Tests/JSON_Tests.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class JSONTests: XCTestCase {
3939
let traits = try? JSON(["email": "[email protected]"])
4040
let userInfo = UserInfo(anonymousId: "1234", userId: "brandon", traits: traits, referrer: nil)
4141

42-
let encoder = JSONEncoder()
42+
let encoder = JSONEncoder.default
4343
encoder.outputFormatting = .prettyPrinted
4444

4545
do {
@@ -51,6 +51,34 @@ class JSONTests: XCTestCase {
5151
}
5252
}
5353

54+
func testJSONDateHandling() throws {
55+
struct TestStruct: Codable {
56+
let myDate: Date
57+
}
58+
59+
let now = Date(timeIntervalSinceNow: 0)
60+
61+
let test = TestStruct(myDate: now)
62+
let object = try JSON(with: test)
63+
let encoder = JSONEncoder.default
64+
encoder.outputFormatting = .prettyPrinted
65+
66+
do {
67+
let json = try encoder.encode(object)
68+
XCTAssertNotNil(json)
69+
let newTest = try! JSONDecoder.default.decode(TestStruct.self, from: json)
70+
XCTAssertEqual(newTest.myDate.toString(), now.toString())
71+
} catch {
72+
print(error)
73+
XCTFail()
74+
}
75+
76+
let dummyProps = ["myDate": now] // <- conforms to Codable
77+
let j = try! JSON(dummyProps)
78+
let anotherTest: TestStruct! = j.codableValue()
79+
XCTAssertEqual(anotherTest.myDate.toString(), now.toString())
80+
}
81+
5482
func testJSONCollectionTypes() throws {
5583
let testSet: Set = ["1", "2", "3"]
5684
let traits = try! JSON(["type": NSNull(), "preferences": ["bwack"], "key": testSet])
@@ -63,13 +91,13 @@ class JSONTests: XCTestCase {
6391

6492
func testJSONNil() throws {
6593
let traits = try JSON(["type": NSNull(), "preferences": ["bwack"], "key": nil] as [String : Any?])
66-
let encoder = JSONEncoder()
94+
let encoder = JSONEncoder.default
6795
encoder.outputFormatting = .prettyPrinted
6896

6997
do {
7098
let json = try encoder.encode(traits)
7199
XCTAssertNotNil(json)
72-
let decoded = try JSONDecoder().decode(Personal.self, from: json)
100+
let decoded = try JSONDecoder.default.decode(Personal.self, from: json)
73101
XCTAssertNil(decoded.type, "Type should be nil")
74102
}
75103
}
@@ -81,7 +109,7 @@ class JSONTests: XCTestCase {
81109

82110
let test = TestStruct(blah: "hello")
83111
let object = try JSON(with: test)
84-
let encoder = JSONEncoder()
112+
let encoder = JSONEncoder.default
85113
encoder.outputFormatting = .prettyPrinted
86114

87115
do {

0 commit comments

Comments
 (0)