diff --git a/README.md b/README.md index 00bacb1..688483b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,15 @@ print(date.timeIntervalSinceReferenceDate) // output: 710590440.0 ``` +There's a helper function you can use with Foundation's `JSONDecoder`: + +```swift +import Parse3339 + +let decoder = JSONDecoder() +decoder.dateDecodingStrategy = .custom(Parse3339.parseFromDecoder(_:)) +``` + For `Package.swift` snippets and documentation, visit the [Swift Package Index page](https://swiftpackageindex.com/juri/Parse3339). ## Speed and memory usage diff --git a/Sources/Parse3339/Documentation.docc/Documentation.md b/Sources/Parse3339/Documentation.docc/Documentation.md index 2534f63..f8bb91c 100644 --- a/Sources/Parse3339/Documentation.docc/Documentation.md +++ b/Sources/Parse3339/Documentation.docc/Documentation.md @@ -43,3 +43,7 @@ print(date.timeIntervalSinceReferenceDate) ### Parser output - ``Parts`` + +### Codable support + +- ``parseFromDecoder(_:)`` diff --git a/Sources/Parse3339/Parse3339.swift b/Sources/Parse3339/Parse3339.swift index b81e451..cee67da 100644 --- a/Sources/Parse3339/Parse3339.swift +++ b/Sources/Parse3339/Parse3339.swift @@ -308,6 +308,36 @@ public func parse(_ seq: some Sequence) -> Parts? { return nil } +// MARK: JSONDecoder support + +/// Helper function for using Parse3339 with Codable types. +/// +/// Use `parseFromDecoder(_:)` with a custom date decoding strategy. The mechanism depends on +/// the `TopLevelDecoder` implementation. With `JSONDecoder` you can use the `dateDecodingStrategy` +/// property with a `custom` value like this: +/// +/// ```swift +/// let decoder = JSONDecoder() +/// decoder.dateDecodingStrategy = .custom(Parse3339.parseFromDecoder(_:)) +/// ``` +/// +/// `parseFromDecoder` first decodes a `String` and then parses the string. For formats other than JSON you may get +/// better performance by using an implementation that feeds bytes to ``parse(_:)-9on3x``. +public func parseFromDecoder(_ decoder: some Decoder) throws -> Date { + let container = try decoder.singleValueContainer() + let str = try container.decode(String.self) + guard let parsed = parse(str) else { + throw DecodingError.typeMismatch( + Date.self, + DecodingError.Context( + codingPath: [], + debugDescription: "The string '\(str)' could not be parsed as a date" + ) + ) + } + return parsed.date +} + // MARK: - Private private enum ZoneDirection { diff --git a/Tests/Parse3339Tests/Parse3339Tests.swift b/Tests/Parse3339Tests/Parse3339Tests.swift index 89d1364..d97888e 100644 --- a/Tests/Parse3339Tests/Parse3339Tests.swift +++ b/Tests/Parse3339Tests/Parse3339Tests.swift @@ -622,6 +622,46 @@ final class Parse3339Tests: XCTestCase { } } } + + // MARK: Decodable + + func testDecodable() throws { + struct Payload: Codable { + let message: String + let date: Date + } + + let dateComponents = DateComponents( + timeZone: TimeZone(identifier: "Europe/Helsinki"), + year: 2023, + month: 7, + day: 11, + hour: 8, + minute: 49, + second: 0 + ) + let date = Calendar(identifier: .gregorian).date(from: dateComponents)! + let payload = Payload(message: "hello world", date: date) + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + let json = try encoder.encode(payload) + + var didCallParse = false + func wrappedParse(_ decoder: any Decoder) throws -> Date { + didCallParse = true + return try parseFromDecoder(decoder) + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(wrappedParse(_:)) + let decoded = try decoder.decode(Payload.self, from: json) + + XCTAssertTrue(didCallParse) + XCTAssertEqual(decoded.message, "hello world") + XCTAssertEqual(decoded.date, date) + } } let isoFormatter: ISO8601DateFormatter = {