diff --git a/Sources/Segment/Utilities/JSON.swift b/Sources/Segment/Utilities/JSON.swift index 8869854f..63de64e7 100644 --- a/Sources/Segment/Utilities/JSON.swift +++ b/Sources/Segment/Utilities/JSON.swift @@ -9,9 +9,23 @@ import Foundation import JSONSafeEncoder extension JSONDecoder { + enum JSONDecodingError: Error { + case couldNotDecodeDate(String) + } + static var `default`: JSONDecoder { let d = JSONDecoder() - d.dateDecodingStrategy = .formatted(DateFormatter.iso8601) + + d.dateDecodingStrategy = .custom({ decoder throws -> Date in + let stringDate = try decoder.singleValueContainer().decode(String.self) + + guard let date = stringDate.iso8601() else { + throw JSONDecodingError.couldNotDecodeDate(stringDate) + } + + return date + }) + return d } } @@ -19,7 +33,13 @@ extension JSONDecoder { extension JSONSafeEncoder { static var `default`: JSONSafeEncoder { let e = JSONSafeEncoder() - e.dateEncodingStrategy = .formatted(DateFormatter.iso8601) + + e.dateEncodingStrategy = .custom({ date, encoder in + let stringDate = date.iso8601() + var container = encoder.singleValueContainer() + try container.encode(stringDate) + }) + e.nonConformingFloatEncodingStrategy = JSON.jsonNonConformingNumberStrategy return e } diff --git a/Sources/Segment/Utilities/iso8601.swift b/Sources/Segment/Utilities/iso8601.swift index d99830e3..9a3fbb83 100644 --- a/Sources/Segment/Utilities/iso8601.swift +++ b/Sources/Segment/Utilities/iso8601.swift @@ -27,14 +27,3 @@ 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 - }() -} diff --git a/Tests/Segment-Tests/JSON_Tests.swift b/Tests/Segment-Tests/JSON_Tests.swift index 56ee093c..829b73de 100644 --- a/Tests/Segment-Tests/JSON_Tests.swift +++ b/Tests/Segment-Tests/JSON_Tests.swift @@ -56,28 +56,42 @@ class JSONTests: XCTestCase { struct TestStruct: Codable { let myDate: Date } - - let now = Date(timeIntervalSinceNow: 0) - - let test = TestStruct(myDate: now) + + let expectedDateString = "2023-12-14T16:03:14.300Z" + let expectedDate = try XCTUnwrap(expectedDateString.iso8601()) + + let test = TestStruct(myDate: expectedDate) let object = try JSON(with: test) let encoder = JSONSafeEncoder.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()) + XCTAssertEqual(newTest.myDate.toString(), "\"\(expectedDateString)\"") } catch { print(error) XCTFail() } - - let dummyProps = ["myDate": now] // <- conforms to Codable + + let dummyProps = ["myDate": expectedDate] // <- conforms to Codable let j = try! JSON(dummyProps) let anotherTest: TestStruct! = j.codableValue() - XCTAssertEqual(anotherTest.myDate.toString(), now.toString()) + XCTAssertEqual(anotherTest.myDate.toString(), "\"\(expectedDateString)\"") + + // Non-Codable (e.g., Objective-C) interface + + let dictWithDate = ["myDate": expectedDate] + let json = try JSON(dictWithDate) + let value = json["myDate"] + + guard case .string(let actualDateString) = value else { + XCTFail() + return + } + + XCTAssertEqual(expectedDateString, actualDateString) } func testJSONCollectionTypes() throws {