From d4716dc61959c6a9d61a0cfa027b683faf656ac7 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 20 Nov 2023 20:46:05 -0800 Subject: [PATCH 1/2] Fix user-specified dates not being output in ISO8601 --- Sources/Segment/ObjC/ObjCAnalytics.swift | 2 +- Sources/Segment/ObjC/ObjCConfiguration.swift | 4 +-- Sources/Segment/Settings.swift | 4 +-- Sources/Segment/Utilities/HTTPClient.swift | 2 +- Sources/Segment/Utilities/JSON.swift | 14 +++++--- Sources/Segment/Utilities/iso8601.swift | 28 ++++++++++++++- Tests/Segment-Tests/Analytics_Tests.swift | 6 ++-- Tests/Segment-Tests/JSON_Tests.swift | 36 +++++++++++++++++--- 8 files changed, 78 insertions(+), 18 deletions(-) diff --git a/Sources/Segment/ObjC/ObjCAnalytics.swift b/Sources/Segment/ObjC/ObjCAnalytics.swift index 9db0b065..8cfc0f54 100644 --- a/Sources/Segment/ObjC/ObjCAnalytics.swift +++ b/Sources/Segment/ObjC/ObjCAnalytics.swift @@ -164,7 +164,7 @@ extension ObjCAnalytics { var result: [String: Any]? = nil if let system: System = analytics.store.currentState() { do { - let encoder = JSONEncoder() + let encoder = JSONEncoder.default let json = try encoder.encode(system.settings) if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] { result = r diff --git a/Sources/Segment/ObjC/ObjCConfiguration.swift b/Sources/Segment/ObjC/ObjCConfiguration.swift index 83ae35d1..f07324f6 100644 --- a/Sources/Segment/ObjC/ObjCConfiguration.swift +++ b/Sources/Segment/ObjC/ObjCConfiguration.swift @@ -75,7 +75,7 @@ public class ObjCConfiguration: NSObject { get { var result = [String: Any]() do { - let encoder = JSONEncoder() + let encoder = JSONEncoder.default let json = try encoder.encode(configuration.values.defaultSettings) if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] { result = r @@ -89,7 +89,7 @@ public class ObjCConfiguration: NSObject { set(value) { do { let json = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted) - let decoder = JSONDecoder() + let decoder = JSONDecoder.default let settings = try decoder.decode(Settings.self, from: json) configuration.defaultSettings(settings) } catch { diff --git a/Sources/Segment/Settings.swift b/Sources/Segment/Settings.swift index 7bd87207..5a52e164 100644 --- a/Sources/Segment/Settings.swift +++ b/Sources/Segment/Settings.swift @@ -46,7 +46,7 @@ public struct Settings: Codable { static public func load(from url: URL?) -> Settings? { guard let url = url else { return nil } guard let data = try? Data(contentsOf: url) else { return nil } - let settings = try? JSONDecoder().decode(Settings.self, from: data) + let settings = try? JSONDecoder.default.decode(Settings.self, from: data) return settings } @@ -80,7 +80,7 @@ public struct Settings: Codable { var result: T? = nil guard let settings = integrations?.dictionaryValue else { return nil } if let dict = settings[key], let jsonData = try? JSONSerialization.data(withJSONObject: dict) { - result = try? JSONDecoder().decode(T.self, from: jsonData) + result = try? JSONDecoder.default.decode(T.self, from: jsonData) } return result } diff --git a/Sources/Segment/Utilities/HTTPClient.swift b/Sources/Segment/Utilities/HTTPClient.swift index 36ef6245..f3d9a509 100644 --- a/Sources/Segment/Utilities/HTTPClient.swift +++ b/Sources/Segment/Utilities/HTTPClient.swift @@ -118,7 +118,7 @@ public class HTTPClient { } do { - let responseJSON = try JSONDecoder().decode(Settings.self, from: data) + let responseJSON = try JSONDecoder.default.decode(Settings.self, from: data) completion(true, responseJSON) } catch { self?.analytics?.reportInternalError(AnalyticsError.jsonUnableToDeserialize(error)) diff --git a/Sources/Segment/Utilities/JSON.swift b/Sources/Segment/Utilities/JSON.swift index 9eddb299..5ef8f224 100644 --- a/Sources/Segment/Utilities/JSON.swift +++ b/Sources/Segment/Utilities/JSON.swift @@ -35,7 +35,7 @@ public enum JSON: Equatable { // For Value types public init(with value: T) throws { - let encoder = JSONEncoder() + let encoder = JSONEncoder.default let json = try encoder.encode(value) let output = try JSONSerialization.jsonObject(with: json) try self.init(output) @@ -58,6 +58,8 @@ public enum JSON: Equatable { // handle swift types case Optional.none: self = .null + case let date as Date: + self = .string(date.iso8601()) case let url as URL: self = .string(url.absoluteString) case let string as String: @@ -134,7 +136,7 @@ extension Encodable { public func toString(pretty: Bool) -> String { var returnString = "" do { - let encoder = JSONEncoder() + let encoder = JSONEncoder.default if pretty { encoder.outputFormatting = .prettyPrinted } @@ -182,7 +184,11 @@ extension JSON { public func codableValue() -> T? { var result: T? = nil if let dict = dictionaryValue, let jsonData = try? JSONSerialization.data(withJSONObject: dict) { - result = try? JSONDecoder().decode(T.self, from: jsonData) + do { + result = try JSONDecoder.default.decode(T.self, from: jsonData) + } catch { + print(error) + } } return result } @@ -402,7 +408,7 @@ extension JSON { if let v = value as? [String: Any] { if let jsonData = try? JSONSerialization.data(withJSONObject: v) { do { - result = try JSONDecoder().decode(T.self, from: jsonData) + result = try JSONDecoder.default.decode(T.self, from: jsonData) } catch { Analytics.segmentLog(message: "Unable to decode object (\(keyPath)) to a Codable: \(error)", kind: .error) } diff --git a/Sources/Segment/Utilities/iso8601.swift b/Sources/Segment/Utilities/iso8601.swift index 9bc03c44..1a098ce8 100644 --- a/Sources/Segment/Utilities/iso8601.swift +++ b/Sources/Segment/Utilities/iso8601.swift @@ -8,7 +8,6 @@ import Foundation enum SegmentISO8601DateFormatter { - static let shared: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions.update(with: .withFractionalSeconds) @@ -28,3 +27,30 @@ internal extension String { return SegmentISO8601DateFormatter.shared.date(from: self) } } + +extension DateFormatter { + static let iso8601: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + formatter.calendar = Calendar(identifier: .iso8601) + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() +} + +extension JSONDecoder { + static var `default`: JSONDecoder { + let d = JSONDecoder() + d.dateDecodingStrategy = .formatted(DateFormatter.iso8601) + return d + } +} + +extension JSONEncoder { + static var `default`: JSONEncoder { + let e = JSONEncoder() + e.dateEncodingStrategy = .formatted(DateFormatter.iso8601) + return e + } +} diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 895262d7..36f733db 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -692,7 +692,7 @@ final class Analytics_Tests: XCTestCase { } - func testAsyncOperatingMode() { + func testAsyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_asyncMode") .flushInterval(9999) @@ -721,7 +721,7 @@ final class Analytics_Tests: XCTestCase { XCTAssertEqual(analytics.pendingUploads!.count, 0) } - func testSyncOperatingMode() { + func testSyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_syncMode") .flushInterval(9999) @@ -755,7 +755,7 @@ final class Analytics_Tests: XCTestCase { XCTAssertEqual(analytics.pendingUploads!.count, 0) } - func testFindAll() { + func testFindAll() throws { let analytics = Analytics(configuration: Configuration(writeKey: "testFindAll") .flushInterval(9999) .flushAt(9999) diff --git a/Tests/Segment-Tests/JSON_Tests.swift b/Tests/Segment-Tests/JSON_Tests.swift index 9bc35adc..f71c5d63 100644 --- a/Tests/Segment-Tests/JSON_Tests.swift +++ b/Tests/Segment-Tests/JSON_Tests.swift @@ -39,7 +39,7 @@ class JSONTests: XCTestCase { let traits = try? JSON(["email": "blah@blah.com"]) let userInfo = UserInfo(anonymousId: "1234", userId: "brandon", traits: traits, referrer: nil) - let encoder = JSONEncoder() + let encoder = JSONEncoder.default encoder.outputFormatting = .prettyPrinted do { @@ -51,6 +51,34 @@ class JSONTests: XCTestCase { } } + func testJSONDateHandling() throws { + struct TestStruct: Codable { + let myDate: Date + } + + let now = Date.now + + let test = TestStruct(myDate: now) + let object = try JSON(with: test) + let encoder = JSONEncoder.default + encoder.outputFormatting = .prettyPrinted + + do { + let json = try encoder.encode(object) + XCTAssertNotNil(json) + let newTest = try! JSONDecoder.default.decode(TestStruct.self, from: json) + XCTAssertEqual(newTest.myDate.toString(), now.toString()) + } catch { + print(error) + XCTFail() + } + + let dummyProps = ["myDate": now] // <- conforms to Codable + let j = try! JSON(dummyProps) + let anotherTest: TestStruct! = j.codableValue() + XCTAssertEqual(anotherTest.myDate.toString(), now.toString()) + } + func testJSONCollectionTypes() throws { let testSet: Set = ["1", "2", "3"] let traits = try! JSON(["type": NSNull(), "preferences": ["bwack"], "key": testSet]) @@ -63,13 +91,13 @@ class JSONTests: XCTestCase { func testJSONNil() throws { let traits = try JSON(["type": NSNull(), "preferences": ["bwack"], "key": nil] as [String : Any?]) - let encoder = JSONEncoder() + let encoder = JSONEncoder.default encoder.outputFormatting = .prettyPrinted do { let json = try encoder.encode(traits) XCTAssertNotNil(json) - let decoded = try JSONDecoder().decode(Personal.self, from: json) + let decoded = try JSONDecoder.default.decode(Personal.self, from: json) XCTAssertNil(decoded.type, "Type should be nil") } } @@ -81,7 +109,7 @@ class JSONTests: XCTestCase { let test = TestStruct(blah: "hello") let object = try JSON(with: test) - let encoder = JSONEncoder() + let encoder = JSONEncoder.default encoder.outputFormatting = .prettyPrinted do { From 331f4bda5c5631237580c8d96abe7372d2e7edcf Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 20 Nov 2023 20:52:18 -0800 Subject: [PATCH 2/2] Who knew `now` was such a recent swift addition. --- Tests/Segment-Tests/JSON_Tests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Segment-Tests/JSON_Tests.swift b/Tests/Segment-Tests/JSON_Tests.swift index f71c5d63..ecfe41d3 100644 --- a/Tests/Segment-Tests/JSON_Tests.swift +++ b/Tests/Segment-Tests/JSON_Tests.swift @@ -56,7 +56,7 @@ class JSONTests: XCTestCase { let myDate: Date } - let now = Date.now + let now = Date(timeIntervalSinceNow: 0) let test = TestStruct(myDate: now) let object = try JSON(with: test)