From e2b159eb12057a1949a72931f6bb6c1ecb375d1c Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 18 Nov 2024 10:27:11 +1100 Subject: [PATCH 01/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index cfae634..a01aca9 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@main + - uses: drekka/swift-coverage-action@develop with: show-all-files: true From ef8a6c2b08abdc0a02f60019ee214ab8260fd363 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 18 Nov 2024 11:32:06 +1100 Subject: [PATCH 02/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a01aca9..e7597bd 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@develop + - uses: drekka/swift-coverage-action@1.1 with: show-all-files: true From e861577e074b4cf22ea893f3390375918997e611 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 18 Nov 2024 11:32:41 +1100 Subject: [PATCH 03/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e7597bd..222c8cf 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@1.1 + - uses: drekka/swift-coverage-action@v1.1 with: show-all-files: true From 5e04eb801dc13abe556e5852f154387c378ad350 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 18 Nov 2024 13:14:07 +1100 Subject: [PATCH 04/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 222c8cf..a01aca9 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@v1.1 + - uses: drekka/swift-coverage-action@develop with: show-all-files: true From 91fecadaec7c85c056811d37d6e2e72103e60e05 Mon Sep 17 00:00:00 2001 From: drekka Date: Mon, 18 Nov 2024 14:41:02 +1100 Subject: [PATCH 05/26] Removing all file header comments --- Sources/Day+Codable.swift | 7 ------- Sources/Day+Conversions.swift | 7 ------- Sources/Day+Core.swift | 7 ------- Sources/Day+CustomStringConvertable.swift | 7 ------- Sources/Day+Formatted.swift | 7 ------- Sources/Day+Functions.swift | 7 ------- Sources/Day+Operations.swift | 7 ------- Sources/Day+Strideable.swift | 7 ------- Sources/Day.swift | 4 ---- Sources/DayComponents.swift | 7 ------- Sources/Property wrappers/CustomISO8601.swift | 7 ------- Sources/Property wrappers/CustomISO8601Configurator.swift | 7 ------- Sources/Property wrappers/DateString.swift | 7 ------- Sources/Property wrappers/DateStringConfigurator.swift | 7 ------- Sources/Property wrappers/Day+DateStringCodable.swift | 7 ------- Sources/Property wrappers/Day+EpochCodable.swift | 7 ------- Sources/Property wrappers/Day+ISO8601Codable.swift | 7 ------- Sources/Property wrappers/EpochMilliseconds.swift | 7 ------- Sources/Property wrappers/EpochSeconds.swift | 7 ------- Sources/Property wrappers/ISO8601.swift | 7 ------- Tests/DayCodableTests.swift | 7 ------- Tests/DayConversionTests.swift | 7 ------- Tests/DayCoreTests.swift | 7 ------- Tests/DayCustomStringConvertableTests.swift | 7 ------- Tests/DayFormattedTests.swift | 7 ------- Tests/DayFunctionsTests.swift | 7 ------- Tests/DayOperationsTests.swift | 7 ------- Tests/DayStrideableTests.swift | 7 ------- Tests/DayTests.swift | 4 ---- Tests/Property wrappers/CustomISO8601Tests.swift | 7 ------- Tests/Property wrappers/DateStringTests.swift | 7 ------- Tests/Property wrappers/EpochMillisecondsTests.swift | 7 ------- Tests/Property wrappers/EpochSecondsTests.swift | 7 ------- Tests/Property wrappers/ISO8601Tests.swift | 7 ------- 34 files changed, 232 deletions(-) diff --git a/Sources/Day+Codable.swift b/Sources/Day+Codable.swift index 34bbccf..1802992 100644 --- a/Sources/Day+Codable.swift +++ b/Sources/Day+Codable.swift @@ -1,10 +1,3 @@ -// -// Day+Codable.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import Foundation /// Encoding and decding stores the internal number of days since 1970 value. diff --git a/Sources/Day+Conversions.swift b/Sources/Day+Conversions.swift index 04cd169..c5742f2 100644 --- a/Sources/Day+Conversions.swift +++ b/Sources/Day+Conversions.swift @@ -1,10 +1,3 @@ -// -// Day+Conversions.swift -// -// -// Created by Derek Clarkson on 6/12/2023. -// - import Foundation public extension Day { diff --git a/Sources/Day+Core.swift b/Sources/Day+Core.swift index 0aba4ef..b4fc872 100644 --- a/Sources/Day+Core.swift +++ b/Sources/Day+Core.swift @@ -1,10 +1,3 @@ -// -// Day+Hashable.swift -// -// -// Created by Derek Clarkson on 10/1/2024. -// - import Foundation extension Day: Hashable { diff --git a/Sources/Day+CustomStringConvertable.swift b/Sources/Day+CustomStringConvertable.swift index e1bc2a2..948e0e5 100644 --- a/Sources/Day+CustomStringConvertable.swift +++ b/Sources/Day+CustomStringConvertable.swift @@ -1,10 +1,3 @@ -// -// Day+CustomStringConvertable.swift -// -// -// Created by Derek Clarkson on 15/1/2024. -// - import Foundation extension Day: CustomStringConvertible { diff --git a/Sources/Day+Formatted.swift b/Sources/Day+Formatted.swift index a32fc15..5954430 100644 --- a/Sources/Day+Formatted.swift +++ b/Sources/Day+Formatted.swift @@ -1,10 +1,3 @@ -// -// Day+Formatted.swift -// -// -// Created by Derek Clarkson on 6/12/2023. -// - import Foundation public extension Day { diff --git a/Sources/Day+Functions.swift b/Sources/Day+Functions.swift index 9118c6b..5430f52 100644 --- a/Sources/Day+Functions.swift +++ b/Sources/Day+Functions.swift @@ -1,10 +1,3 @@ -// -// Day+Functions.swift -// -// -// Created by Derek Clarkson on 16/1/2024. -// - import Foundation public extension Day { diff --git a/Sources/Day+Operations.swift b/Sources/Day+Operations.swift index 934b2db..fb0761c 100644 --- a/Sources/Day+Operations.swift +++ b/Sources/Day+Operations.swift @@ -1,10 +1,3 @@ -// -// Day+Operations.swift -// -// -// Created by Derek Clarkson on 11/12/2023. -// - import Foundation public extension Day { diff --git a/Sources/Day+Strideable.swift b/Sources/Day+Strideable.swift index ecfd657..e70ba41 100644 --- a/Sources/Day+Strideable.swift +++ b/Sources/Day+Strideable.swift @@ -1,10 +1,3 @@ -// -// Day+Stridable.swift -// -// -// Created by Derek Clarkson on 15/1/2024. -// - import Foundation extension Day: Strideable { diff --git a/Sources/Day.swift b/Sources/Day.swift index 43e5a26..d8bb34c 100755 --- a/Sources/Day.swift +++ b/Sources/Day.swift @@ -1,7 +1,3 @@ -// -// Copyright © Derek Clarkson. All rights reserved. -// - import Foundation /// Simple alias to help code readability. diff --git a/Sources/DayComponents.swift b/Sources/DayComponents.swift index c5320a2..7277ac3 100644 --- a/Sources/DayComponents.swift +++ b/Sources/DayComponents.swift @@ -1,10 +1,3 @@ -// -// DayComponents.swift -// -// -// Created by Derek Clarkson on 5/11/2023. -// - import Foundation /// Similar to Swift's ``DateComponents`` in that it contains the individual components of a ``Day``. diff --git a/Sources/Property wrappers/CustomISO8601.swift b/Sources/Property wrappers/CustomISO8601.swift index 8df56cb..ab10ac6 100644 --- a/Sources/Property wrappers/CustomISO8601.swift +++ b/Sources/Property wrappers/CustomISO8601.swift @@ -1,10 +1,3 @@ -// -// CustomISO8601Day.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Identifies a ``Day`` property that reads and writes from an ISO8601 formatted string. diff --git a/Sources/Property wrappers/CustomISO8601Configurator.swift b/Sources/Property wrappers/CustomISO8601Configurator.swift index 8ab85cd..dc5bb3b 100644 --- a/Sources/Property wrappers/CustomISO8601Configurator.swift +++ b/Sources/Property wrappers/CustomISO8601Configurator.swift @@ -1,10 +1,3 @@ -// -// ISO8601CodingStrategy.swift -// -// -// Created by Derek Clarkson on 10/1/2024. -// - import Foundation /// Sets up the ``ISO8601DateFormatter`` used for decoding and encoding date strings. diff --git a/Sources/Property wrappers/DateString.swift b/Sources/Property wrappers/DateString.swift index 65167ac..01521d6 100644 --- a/Sources/Property wrappers/DateString.swift +++ b/Sources/Property wrappers/DateString.swift @@ -1,10 +1,3 @@ -// -// CustomISO8601Day.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Identifies a ``Day`` property that reads and writes from date strings using a configured ``DateFormatter``. diff --git a/Sources/Property wrappers/DateStringConfigurator.swift b/Sources/Property wrappers/DateStringConfigurator.swift index 789f80b..ba62fbf 100644 --- a/Sources/Property wrappers/DateStringConfigurator.swift +++ b/Sources/Property wrappers/DateStringConfigurator.swift @@ -1,10 +1,3 @@ -// -// ISO8601CodingStrategy.swift -// -// -// Created by Derek Clarkson on 10/1/2024. -// - import Foundation /// Sets up the ``DateFormatter`` used for decoding and encoding date strings. diff --git a/Sources/Property wrappers/Day+DateStringCodable.swift b/Sources/Property wrappers/Day+DateStringCodable.swift index 02ecdad..0d9c4e3 100644 --- a/Sources/Property wrappers/Day+DateStringCodable.swift +++ b/Sources/Property wrappers/Day+DateStringCodable.swift @@ -1,10 +1,3 @@ -// -// DayCodable.swift -// -// -// Created by Derek Clarkson on 29/1/2024. -// - import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. diff --git a/Sources/Property wrappers/Day+EpochCodable.swift b/Sources/Property wrappers/Day+EpochCodable.swift index 6e3999c..776e343 100644 --- a/Sources/Property wrappers/Day+EpochCodable.swift +++ b/Sources/Property wrappers/Day+EpochCodable.swift @@ -1,10 +1,3 @@ -// -// DayCodable.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. diff --git a/Sources/Property wrappers/Day+ISO8601Codable.swift b/Sources/Property wrappers/Day+ISO8601Codable.swift index af3afd0..4cada2d 100644 --- a/Sources/Property wrappers/Day+ISO8601Codable.swift +++ b/Sources/Property wrappers/Day+ISO8601Codable.swift @@ -1,10 +1,3 @@ -// -// DayCodable.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. diff --git a/Sources/Property wrappers/EpochMilliseconds.swift b/Sources/Property wrappers/EpochMilliseconds.swift index 54fd9a6..187a19e 100644 --- a/Sources/Property wrappers/EpochMilliseconds.swift +++ b/Sources/Property wrappers/EpochMilliseconds.swift @@ -1,10 +1,3 @@ -// -// EpochDay.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Identifies a ``Day`` property that reads and writes from an epoch time value expressed in seconds. diff --git a/Sources/Property wrappers/EpochSeconds.swift b/Sources/Property wrappers/EpochSeconds.swift index c22daaa..819a440 100644 --- a/Sources/Property wrappers/EpochSeconds.swift +++ b/Sources/Property wrappers/EpochSeconds.swift @@ -1,10 +1,3 @@ -// -// EpochDay.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Identifies a ``Day`` property that reads and writes from an epoch time value expressed in seconds. diff --git a/Sources/Property wrappers/ISO8601.swift b/Sources/Property wrappers/ISO8601.swift index 3cddce6..f2f5ab8 100644 --- a/Sources/Property wrappers/ISO8601.swift +++ b/Sources/Property wrappers/ISO8601.swift @@ -1,10 +1,3 @@ -// -// ISO8601Day.swift -// -// -// Created by Derek Clarkson on 9/1/2024. -// - import Foundation /// Identifies a ``Day`` property that reads and writes from an ISO8601 formatted string. diff --git a/Tests/DayCodableTests.swift b/Tests/DayCodableTests.swift index f29fec7..f13d2de 100644 --- a/Tests/DayCodableTests.swift +++ b/Tests/DayCodableTests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayConversionTests.swift b/Tests/DayConversionTests.swift index bf073e4..000b5d8 100644 --- a/Tests/DayConversionTests.swift +++ b/Tests/DayConversionTests.swift @@ -1,10 +1,3 @@ -// -// DayConversionTests.swift -// -// -// Created by Derek Clarkson on 6/12/2023. -// - import XCTest import DayType import Nimble diff --git a/Tests/DayCoreTests.swift b/Tests/DayCoreTests.swift index 0f2703b..98b676c 100644 --- a/Tests/DayCoreTests.swift +++ b/Tests/DayCoreTests.swift @@ -1,10 +1,3 @@ -// -// DayOperationsTests.swift -// -// -// Created by Derek Clarkson on 12/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayCustomStringConvertableTests.swift b/Tests/DayCustomStringConvertableTests.swift index 5d9fcca..3614bbd 100644 --- a/Tests/DayCustomStringConvertableTests.swift +++ b/Tests/DayCustomStringConvertableTests.swift @@ -1,10 +1,3 @@ -// -// DayCustomStringConvertableTests.swift -// -// -// Created by Derek Clarkson on 15/1/2024. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayFormattedTests.swift b/Tests/DayFormattedTests.swift index 09f95be..3816a9b 100644 --- a/Tests/DayFormattedTests.swift +++ b/Tests/DayFormattedTests.swift @@ -1,10 +1,3 @@ -// -// DayFormattedTests.swift -// -// -// Created by Derek Clarkson on 7/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayFunctionsTests.swift b/Tests/DayFunctionsTests.swift index c169478..7401618 100644 --- a/Tests/DayFunctionsTests.swift +++ b/Tests/DayFunctionsTests.swift @@ -1,10 +1,3 @@ -// -// DayFunctionsTests.swift -// -// -// Created by Derek Clarkson on 16/1/2024. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayOperationsTests.swift b/Tests/DayOperationsTests.swift index 48b4ce7..71147da 100644 --- a/Tests/DayOperationsTests.swift +++ b/Tests/DayOperationsTests.swift @@ -1,10 +1,3 @@ -// -// DayOperationsTests.swift -// -// -// Created by Derek Clarkson on 12/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/DayStrideableTests.swift b/Tests/DayStrideableTests.swift index e1bc4fe..a59bf9a 100644 --- a/Tests/DayStrideableTests.swift +++ b/Tests/DayStrideableTests.swift @@ -1,10 +1,3 @@ -// -// Day+Stridable.swift -// -// -// Created by Derek Clarkson on 15/1/2024. -// - import Foundation import DayType import XCTest diff --git a/Tests/DayTests.swift b/Tests/DayTests.swift index 0c228af..c5d9dba 100755 --- a/Tests/DayTests.swift +++ b/Tests/DayTests.swift @@ -1,7 +1,3 @@ -// -// Copyright © Derek Clarkson. All rights reserved. -// - import DayType import Foundation import Nimble diff --git a/Tests/Property wrappers/CustomISO8601Tests.swift b/Tests/Property wrappers/CustomISO8601Tests.swift index 75d286b..7606683 100644 --- a/Tests/Property wrappers/CustomISO8601Tests.swift +++ b/Tests/Property wrappers/CustomISO8601Tests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index f0ffa96..09762f5 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/Property wrappers/EpochMillisecondsTests.swift b/Tests/Property wrappers/EpochMillisecondsTests.swift index 124082e..616ecb2 100644 --- a/Tests/Property wrappers/EpochMillisecondsTests.swift +++ b/Tests/Property wrappers/EpochMillisecondsTests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/Property wrappers/EpochSecondsTests.swift b/Tests/Property wrappers/EpochSecondsTests.swift index 1cac6a1..7755e08 100644 --- a/Tests/Property wrappers/EpochSecondsTests.swift +++ b/Tests/Property wrappers/EpochSecondsTests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest diff --git a/Tests/Property wrappers/ISO8601Tests.swift b/Tests/Property wrappers/ISO8601Tests.swift index 6eb5fe4..090cf2b 100644 --- a/Tests/Property wrappers/ISO8601Tests.swift +++ b/Tests/Property wrappers/ISO8601Tests.swift @@ -1,10 +1,3 @@ -// -// DayCodableTests.swift -// -// -// Created by Derek Clarkson on 9/12/2023. -// - import DayType import Nimble import XCTest From 891829ed60c490f767f3be6aad39756a8d7f1cfa Mon Sep 17 00:00:00 2001 From: drekka Date: Mon, 18 Nov 2024 17:02:44 +1100 Subject: [PATCH 06/26] wip --- Package.swift | 2 +- Tests/DayCodableTests.swift | 21 ++++---- Tests/DayConversionTests.swift | 41 ++++++++-------- Tests/Property wrappers/DateStringTests.swift | 48 ++++++++----------- Tests/Tags.swift | 6 +++ 5 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 Tests/Tags.swift diff --git a/Package.swift b/Package.swift index 5989e86..fa1ad78 100755 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription diff --git a/Tests/DayCodableTests.swift b/Tests/DayCodableTests.swift index f13d2de..cf9e6b7 100644 --- a/Tests/DayCodableTests.swift +++ b/Tests/DayCodableTests.swift @@ -1,14 +1,16 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayCodableTests: XCTestCase { +@Suite("Day is Codable") +struct DayCodableTests { - struct DummyType: Codable { + private struct DummyType: Codable { let abc: Day } - func testBaseDecoding() throws { + @Test("Decoding") + func decoding() throws { let json = """ { @@ -18,13 +20,14 @@ class DayCodableTests: XCTestCase { let decoder = JSONDecoder() let day = try decoder.decode(DummyType.self, from: json.data(using: .utf8)!) - expect(day.abc.daysSince1970) == 19455 + #expect(day.abc.daysSince1970 == 19455) } - func testEncoding() throws { + @Test("Encoding") + func encoding() throws { let obj = DummyType(abc: Day(daysSince1970: 19455)) let encoder = JSONEncoder() - let encoded = try String(data: encoder.encode(obj), encoding: .utf8) - expect(encoded).to(contain("\"abc\":19455")) + let encoded = try #require(String(data: encoder.encode(obj), encoding: .utf8)) + #expect(encoded == #"{"abc":19455}"#) } } diff --git a/Tests/DayConversionTests.swift b/Tests/DayConversionTests.swift index 000b5d8..e093413 100644 --- a/Tests/DayConversionTests.swift +++ b/Tests/DayConversionTests.swift @@ -1,22 +1,25 @@ -import XCTest import DayType -import Nimble +import Foundation +import Testing -class DayConversionTests: XCTestCase { +@Suite("Day to Date conversions") +struct DayConversionTests { - func testDateInCalendarCurrent() { - let day = Day(2000,1,1) + @Test("To a Date") + func toDate() { + let day = Day(2000, 1, 1) let date = day.date() let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour], from: date) - expect(dateComponents.year) == 2000 - expect(dateComponents.month) == 1 - expect(dateComponents.day) == 1 - expect(dateComponents.hour) == 0 + #expect(dateComponents.year == 2000) + #expect(dateComponents.month == 1) + #expect(dateComponents.day == 1) + #expect(dateComponents.hour == 0) } + @Test("Time zones are correct") func testDateInTimeZone() { - let day = Day(2000,1,1) + let day = Day(2000, 1, 1) // Get the date of the day in the Melbourne time zone. let melbourne = TimeZone(secondsFromGMT: 11 * 3600)! @@ -27,20 +30,20 @@ class DayConversionTests: XCTestCase { let gmtDate = day.date(timeZone: gmt) // We expect that the two points in time are different. - expect(melbourneDate.timeIntervalSince1970) != gmtDate.timeIntervalSince1970 + #expect(melbourneDate.timeIntervalSince1970 != gmtDate.timeIntervalSince1970) // Check that the Melbourne date is midnight in the Melbourne time zone. let melbourneComponentsInMelbourne = Calendar.current.dateComponents(in: melbourne, from: melbourneDate) - expect(melbourneComponentsInMelbourne.year) == 2000 - expect(melbourneComponentsInMelbourne.month) == 1 - expect(melbourneComponentsInMelbourne.day) == 1 - expect(melbourneComponentsInMelbourne.hour) == 0 + #expect(melbourneComponentsInMelbourne.year == 2000) + #expect(melbourneComponentsInMelbourne.month == 1) + #expect(melbourneComponentsInMelbourne.day == 1) + #expect(melbourneComponentsInMelbourne.hour == 0) // Check that the GMT date is midnight in the GMT time zone. let gmtComponentsInGMT = Calendar.current.dateComponents(in: gmt, from: gmtDate) - expect(gmtComponentsInGMT.year) == 2000 - expect(gmtComponentsInGMT.month) == 1 - expect(gmtComponentsInGMT.day) == 1 - expect(gmtComponentsInGMT.hour) == 0 + #expect(gmtComponentsInGMT.year == 2000) + #expect(gmtComponentsInGMT.month == 1) + #expect(gmtComponentsInGMT.day == 1) + #expect(gmtComponentsInGMT.hour == 0) } } diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 09762f5..4c8f683 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -1,40 +1,34 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -// MARK: - ISO8601 Decoding - -private struct DateStringContainer: Codable where Configurator: DateStringConfigurator { +private struct DayContainer: Codable where Configurator: DateStringConfigurator { @DateString var d1: Day - init(d1: Day) { - self.d1 = d1 - } } -private struct DateStringOptionalContainer: Codable where Configurator: DateStringConfigurator { +private struct OptionalDayContainer: Codable where Configurator: DateStringConfigurator { @DateString var d1: Day? - init(d1: Day?) { - self.d1 = d1 - } } -class DateStringTests: XCTestCase { - func testDecodingISO8601DateString() throws { +struct DateStringTests { + + @Test("Decoding am ISO8601 string") + func decodingISO8601DateString() throws { let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(DateStringContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) } func testDecodingAustralianDateStrings() throws { let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(DateStringContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == Day(2012, 02, 01) } func testDecodingAmericanDateStrings() throws { let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(DateStringContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == Day(2012, 02, 01) } } @@ -43,25 +37,25 @@ class CodableAsDateStroingOptionalTests: XCTestCase { func testDecodingISO8601DateString() throws { let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(DateStringOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == Day(2012, 02, 01) } func testDecodingAustralianDateStrings() throws { let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(DateStringOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == Day(2012, 02, 01) } func testDecodingAmericanDateStrings() throws { let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(DateStringOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == Day(2012, 02, 01) } func testDecodingNilAustralianDateStrings() throws { let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(DateStringOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) expect(result.d1) == nil } @@ -69,9 +63,9 @@ class CodableAsDateStroingOptionalTests: XCTestCase { let json = #"{"d1": "xxx"}"# let decoder = JSONDecoder() - expect(try decoder.decode(DateStringOptionalContainer.self, from: json.data(using: .utf8)!)) + expect(try decoder.decode(OptionalDayContainer.self, from: json.data(using: .utf8)!)) .to(throwError { error in - guard case DecodingError.dataCorrupted(let context) = error else { + guard case let DecodingError.dataCorrupted(context) = error else { fail("Incorrect error \(error)") return } @@ -84,7 +78,7 @@ class CodableAsDateStroingOptionalTests: XCTestCase { class CodableAsDateStringEncodingTests: XCTestCase { func testEncoding() throws { - let instance = DateStringContainer(d1: Day(2012, 02, 01)) + let instance = DayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) @@ -95,7 +89,7 @@ class CodableAsDateStringEncodingTests: XCTestCase { class CodableAsDateStringOptionalTests: XCTestCase { func testEncoding() throws { - let instance = DateStringOptionalContainer(d1: Day(2012, 02, 01)) + let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) @@ -103,7 +97,7 @@ class CodableAsDateStringOptionalTests: XCTestCase { } func testEncodingNil() throws { - let instance = DateStringOptionalContainer(d1: nil) + let instance = OptionalDayContainer(d1: nil) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) diff --git a/Tests/Tags.swift b/Tests/Tags.swift new file mode 100644 index 0000000..b23ed3a --- /dev/null +++ b/Tests/Tags.swift @@ -0,0 +1,6 @@ +import Testing + +extension Tag { + @Tag static var PropertyWrapper: Self +} + From 384e3c9b5bd68ca810794a9df4fded043a5b0e21 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 15:11:08 +1100 Subject: [PATCH 07/26] Converting to Swift Testing --- Tests/Property wrappers/DateStringTests.swift | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 4c8f683..e6472ac 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -10,97 +10,99 @@ private struct OptionalDayContainer: Codable where Configurator: D @DateString var d1: Day? } +@Suite("Date string decoding") +struct DateStringDecodingTests { -struct DateStringTests { - - @Test("Decoding am ISO8601 string") - func decodingISO8601DateString() throws { + @Test("Decoding an ISO8601 date") + func decodingAnISO8601Date() throws { let json = #"{"d1": "2012-02-01"}"# let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } - func testDecodingAustralianDateStrings() throws { + @Test("Decoding an DMY date") + func decodingADMYDate() throws { let json = #"{"d1": "01/02/2012"}"# let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + #expect(result.d1 == Day(2012, 02, 01)) } - func testDecodingAmericanDateStrings() throws { + @Test("Decoding an MDY date") + func decodingAMDYDate() throws { let json = #"{"d1": "02/01/2012"}"# let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + #expect(result.d1 == Day(2012, 02, 01)) } -} - -class CodableAsDateStroingOptionalTests: XCTestCase { - func testDecodingISO8601DateString() throws { + @Test("Decoding an optional ISO8601 date") + func decodingAnIOptionalISO8601Date() throws { let json = #"{"d1": "2012-02-01"}"# let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + #expect(result.d1 == Day(2012, 02, 01)) } - func testDecodingAustralianDateStrings() throws { + @Test("Decoding an DMY date") + func decodingAnOptionalDMYDate() throws { let json = #"{"d1": "01/02/2012"}"# let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + #expect(result.d1 == Day(2012, 02, 01)) } - func testDecodingAmericanDateStrings() throws { + @Test("Decoding an MDY date") + func decodingAnOptionalMDYDate() throws { let json = #"{"d1": "02/01/2012"}"# let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 01) + #expect(result.d1 == Day(2012, 02, 01)) } - func testDecodingNilAustralianDateStrings() throws { + @Test("Decoding a nil date") + func decodingANilDate() throws { let json = #"{"d1": null}"# let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == nil + #expect(result.d1 == nil) } - func testDecodingInvalidAustralianDateStringsThrows() throws { - - let json = #"{"d1": "xxx"}"# - let decoder = JSONDecoder() - expect(try decoder.decode(OptionalDayContainer.self, from: json.data(using: .utf8)!)) - .to(throwError { error in - guard case let DecodingError.dataCorrupted(context) = error else { - fail("Incorrect error \(error)") - return - } - expect(context.codingPath.map { $0.stringValue }) == ["d1"] - expect(context.debugDescription) == "Unable to read the date string." - }) + @Test("Decoding an invalid date throws an error") + func decodingInvalidDateThrows() throws { + do { + let json = #"{"d1": "xxx"}"# + _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.map(\.stringValue) == ["d1"]) + #expect(context.debugDescription == "Unable to read the date string.") + } catch { + Issue.record("Unexpected error: \(error)") + } } } -class CodableAsDateStringEncodingTests: XCTestCase { +@Suite("Date string encoding") +class DateStringEncodingTests { - func testEncoding() throws { + @Test("Encoding a DMY date") + func dateEncoding() throws { let instance = DayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"01/02/2012"}"# + #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) } -} - -class CodableAsDateStringOptionalTests: XCTestCase { - func testEncoding() throws { + @Test("Encoding an optional DMY date") + func optionalDateEncoding() throws { let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"01/02/2012"}"# + #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) } - func testEncodingNil() throws { + @Test("Encoding an optional DMY date from a nil") + func optionalDateEncodingWithNil() throws { let instance = OptionalDayContainer(d1: nil) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":null}"# + #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) } } From 45eed2d73e619963d27f5909d542f598a90ad2f2 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 15:15:33 +1100 Subject: [PATCH 08/26] Update swift.yml --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a01aca9..827352b 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -16,12 +16,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: szenius/set-timezone@v1.2 + - uses: szenius/set-timezone@v2.0 with: timezoneLinux: "Australia/Melbourne" timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@develop + - uses: drekka/swift-coverage-action@v1.1 with: show-all-files: true From 718c52089fbdc5674e5e1877db82b2eaec5b2c3b Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 15:20:06 +1100 Subject: [PATCH 09/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 827352b..607f172 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@v1.1 + - uses: drekka/swift-coverage-action@develop with: show-all-files: true From e0361494ab078a9351a74aa5e7391db1543245eb Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 17:03:39 +1100 Subject: [PATCH 10/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 607f172..ac4907f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@develop + - uses: drekka/swift-coverage-action@1.2 with: show-all-files: true From fbbc72a07e67421b8fbb2eabff477a15f506f227 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 17:04:48 +1100 Subject: [PATCH 11/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index ac4907f..8e41f88 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@1.2 + - uses: drekka/swift-coverage-action@v1.2 with: show-all-files: true From 72e4429cac2c49b9f9c963019905e20071f88390 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 19 Nov 2024 21:15:00 +1100 Subject: [PATCH 12/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 8e41f88..607f172 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@v1.2 + - uses: drekka/swift-coverage-action@develop with: show-all-files: true From a26480cc1e0470eaa3a6306610444e063a7e1459 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 20 Nov 2024 11:48:53 +1100 Subject: [PATCH 13/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 607f172..110668a 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,6 +22,6 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@develop + - uses: drekka/swift-coverage-action@v1.3 with: show-all-files: true From 96b6525e742454e5131109991a37cfeed2c6288e Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 20 Nov 2024 15:47:20 +1100 Subject: [PATCH 14/26] Switching from Nimble to Swift Testing --- Package.resolved | 32 ---- Package.swift | 2 - .../CustomISO8601Configurator.swift | 2 +- Tests/DayCoreTests.swift | 92 +++++----- Tests/DayCustomStringConvertableTests.swift | 13 +- Tests/DayFormattedTests.swift | 24 ++- Tests/DayFunctionsTests.swift | 23 +-- Tests/DayOperationsTests.swift | 32 ++-- Tests/DayStrideableTests.swift | 25 +-- Tests/DayTests.swift | 45 +++-- .../CustomISO8601Tests.swift | 143 ++++++++-------- Tests/Property wrappers/DateStringTests.swift | 162 +++++++++--------- .../EpochMillisecondsTests.swift | 122 ++++++------- .../Property wrappers/EpochSecondsTests.swift | 122 ++++++------- Tests/Property wrappers/ISO8601Tests.swift | 132 +++++++------- .../Property wrappers/PropertyWrappers.swift | 6 + 16 files changed, 494 insertions(+), 483 deletions(-) delete mode 100755 Package.resolved create mode 100644 Tests/Property wrappers/PropertyWrappers.swift diff --git a/Package.resolved b/Package.resolved deleted file mode 100755 index 143534f..0000000 --- a/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "cwlcatchexception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlCatchException.git", - "state" : { - "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", - "version" : "2.1.2" - } - }, - { - "identity" : "cwlpreconditiontesting", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state" : { - "revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", - "version" : "2.2.0" - } - }, - { - "identity" : "nimble", - "kind" : "remoteSourceControl", - "location" : "https://github.com/quick/nimble", - "state" : { - "revision" : "eb5e3d717224fa0d1f6aff3fc2c5e8e81fa1f728", - "version" : "11.2.2" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index fa1ad78..bbc3633 100755 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,6 @@ let package = Package( .library(name: "DayType", targets: ["DayType"]), ], dependencies: [ - .package(url: "https://github.com/quick/nimble", from: "11.0.0"), ], targets: [ .target( @@ -23,7 +22,6 @@ let package = Package( name: "DayTypeTests", dependencies: [ "DayType", - .product(name: "Nimble", package: "nimble"), ], path: "Tests" ), diff --git a/Sources/Property wrappers/CustomISO8601Configurator.swift b/Sources/Property wrappers/CustomISO8601Configurator.swift index dc5bb3b..c5854fb 100644 --- a/Sources/Property wrappers/CustomISO8601Configurator.swift +++ b/Sources/Property wrappers/CustomISO8601Configurator.swift @@ -16,7 +16,7 @@ public enum ISO8601Config { public static func configure(formatter _: ISO8601DateFormatter) {} } - /// Removes the time zone element from the string. + /// Tells the formatter to ignores timezone values. public enum SansTimeZone: CustomISO8601Configurator { public static func configure(formatter: ISO8601DateFormatter) { formatter.formatOptions.remove(.withTimeZone) diff --git a/Tests/DayCoreTests.swift b/Tests/DayCoreTests.swift index 98b676c..0918117 100644 --- a/Tests/DayCoreTests.swift +++ b/Tests/DayCoreTests.swift @@ -1,61 +1,63 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayHashableTests: XCTestCase { +@Suite("Protocol conformance") +struct DayProtocols { - func testHash() { + @Test("Hash") + func hash() { var days: Set = [Day(2020, 01, 11), Day(2020, 01, 12)] - expect(days.contains(Day(2020, 01, 13))) == false - expect(days.contains(Day(2020, 01, 12))) == true + + #expect(days.contains(Day(2020, 01, 13)) == false) + #expect(days.contains(Day(2020, 01, 12)) == true) // Modify and try again. days.insert(Day(2020, 01, 13)) - expect(days.contains(Day(2020, 01, 13))) == true - expect(days.contains(Day(2020, 01, 12))) == true + #expect(days.count == 3) + #expect(days.contains(Day(2020, 01, 13)) == true) + #expect(days.contains(Day(2020, 01, 12)) == true) // Duplicate check. days.insert(Day(2020, 01, 11)) - expect(days.count) == 3 - } -} - -class DayEquatableTests: XCTestCase { - - func testEquals() { - expect(Day(2020, 3, 12) == Day(2020, 3, 12)) == true - expect(Day(2020, 3, 12) == Day(2001, 1, 5)) == false - } - - func testNotEquals() { - expect(Day(2020, 3, 12) != Day(2001, 1, 5)) == true - expect(Day(2020, 3, 12) != Day(2020, 3, 12)) == false - } -} - -class DayComparableTests: XCTestCase { - - func testGreaterThan() { - expect(Day(2020, 3, 12) > Day(2020, 3, 11)) == true - expect(Day(2020, 3, 12) > Day(2020, 3, 12)) == false - expect(Day(2020, 3, 12) > Day(2020, 3, 13)) == false - } - - func testGreaterThanEquals() { - expect(Day(2020, 3, 12) >= Day(2020, 3, 11)) == true - expect(Day(2020, 3, 12) >= Day(2020, 3, 12)) == true - expect(Day(2020, 3, 12) >= Day(2020, 3, 13)) == false + #expect(days.count == 3) } - func testLessThan() { - expect(Day(2020, 3, 12) < Day(2020, 3, 11)) == false - expect(Day(2020, 3, 12) < Day(2020, 3, 12)) == false - expect(Day(2020, 3, 12) < Day(2020, 3, 13)) == true + @Test("Equatble") + func equals() { + #expect(Day(2020, 3, 12) == Day(2020, 3, 12)) + #expect(Day(2020, 3, 12) != Day(2001, 1, 5)) } - func testLessThanEquals() { - expect(Day(2020, 3, 12) <= Day(2020, 3, 11)) == false - expect(Day(2020, 3, 12) <= Day(2020, 3, 12)) == true - expect(Day(2020, 3, 12) <= Day(2020, 3, 13)) == true + @Suite("Comparable") + struct DayComparableTests { + + @Test(">") + func greaterThan() { + #expect((Day(2020, 3, 12) > Day(2020, 3, 11)) == true) + #expect((Day(2020, 3, 12) > Day(2020, 3, 12)) == false) + #expect((Day(2020, 3, 12) > Day(2020, 3, 13)) == false) + } + + @Test(">=") + func greaterThanEquals() { + #expect((Day(2020, 3, 12) >= Day(2020, 3, 11)) == true) + #expect((Day(2020, 3, 12) >= Day(2020, 3, 12)) == true) + #expect((Day(2020, 3, 12) >= Day(2020, 3, 13)) == false) + } + + @Test("<") + func lessThan() { + #expect((Day(2020, 3, 12) < Day(2020, 3, 11)) == false) + #expect((Day(2020, 3, 12) < Day(2020, 3, 12)) == false) + #expect((Day(2020, 3, 12) < Day(2020, 3, 13)) == true) + } + + @Test("<=") + func lessThanEquals() { + #expect((Day(2020, 3, 12) <= Day(2020, 3, 11)) == false) + #expect((Day(2020, 3, 12) <= Day(2020, 3, 12)) == true) + #expect((Day(2020, 3, 12) <= Day(2020, 3, 13)) == true) + } } } diff --git a/Tests/DayCustomStringConvertableTests.swift b/Tests/DayCustomStringConvertableTests.swift index 3614bbd..7b0549c 100644 --- a/Tests/DayCustomStringConvertableTests.swift +++ b/Tests/DayCustomStringConvertableTests.swift @@ -1,10 +1,9 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayCustomStringConvertableTests: XCTestCase { - func testyDescription() { - let date = DateComponents(calendar: .current, year: 2001, month: 2, day: 3).date! - expect(Day(2001, 2, 3).description) == date.formatted(date: .abbreviated, time: .omitted) - } +@Test("Day description matches Date formatted") +func description() { + let date = DateComponents(calendar: .current, year: 2001, month: 2, day: 3).date! + #expect(Day(2001, 2, 3).description == date.formatted(date: .abbreviated, time: .omitted)) } diff --git a/Tests/DayFormattedTests.swift b/Tests/DayFormattedTests.swift index 3816a9b..a330602 100644 --- a/Tests/DayFormattedTests.swift +++ b/Tests/DayFormattedTests.swift @@ -1,18 +1,16 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayFormattedTests: XCTestCase { +@Test("Day formatted() matches Date formatted strings") +func formatted() { - func testFormatted() { - - let day = Day(2000, 2, 1) - let date = DateComponents(calendar: .current, year: 2000, month: 2, day: 1).date! + let day = Day(2000, 2, 1) + let date = DateComponents(calendar: .current, year: 2000, month: 2, day: 1).date! - expect(day.formatted()) == date.formatted(date: .abbreviated, time: .omitted) - expect(day.formatted(.abbreviated)) == date.formatted(date: .abbreviated, time: .omitted) - expect(day.formatted(.complete)) == date.formatted(date: .complete, time: .omitted) - expect(day.formatted(.long)) == date.formatted(date: .long, time: .omitted) - expect(day.formatted(.numeric)) == date.formatted(date: .numeric, time: .omitted) - } + #expect(day.formatted() == date.formatted(date: .abbreviated, time: .omitted)) + #expect(day.formatted(.abbreviated) == date.formatted(date: .abbreviated, time: .omitted)) + #expect(day.formatted(.complete) == date.formatted(date: .complete, time: .omitted)) + #expect(day.formatted(.long) == date.formatted(date: .long, time: .omitted)) + #expect(day.formatted(.numeric) == date.formatted(date: .numeric, time: .omitted)) } diff --git a/Tests/DayFunctionsTests.swift b/Tests/DayFunctionsTests.swift index 7401618..42c0721 100644 --- a/Tests/DayFunctionsTests.swift +++ b/Tests/DayFunctionsTests.swift @@ -1,17 +1,20 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayFunctionsTests: XCTestCase { +@Suite("Day functions") +struct DayFunctionsTests { - func testDayByAdding() { - expect(Day(2001, 2, 3).day(byAdding: .day, value: 3)) == Day(2001, 2, 6) - expect(Day(2001, 2, 3).day(byAdding: .month, value: 3)) == Day(2001, 5, 3) - expect(Day(2001, 2, 3).day(byAdding: .year, value: 3)) == Day(2004, 2, 3) + @Test("Adding days") + func addingDays() { + #expect(Day(2001, 2, 3).day(byAdding: .day, value: 3) == Day(2001, 2, 6)) + #expect(Day(2001, 2, 3).day(byAdding: .month, value: 3) == Day(2001, 5, 3)) + #expect(Day(2001, 2, 3).day(byAdding: .year, value: 3) == Day(2004, 2, 3)) } - func testDayByAddingRolling() { - expect(Day(2001, 2, 3).day(byAdding: .day, value: 55)) == Day(2001, 3, 30) - expect(Day(2001, 2, 3).day(byAdding: .month, value: 55)) == Day(2005, 9, 10) + @Test("Adding and rolling days") + func rollingDays() { + #expect(Day(2001, 2, 3).day(byAdding: .day, value: 55) == Day(2001, 3, 30)) + #expect(Day(2001, 2, 3).day(byAdding: .month, value: 55) == Day(2005, 9, 10)) } } diff --git a/Tests/DayOperationsTests.swift b/Tests/DayOperationsTests.swift index 71147da..a5e032f 100644 --- a/Tests/DayOperationsTests.swift +++ b/Tests/DayOperationsTests.swift @@ -1,30 +1,36 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing -class DayOperationTests: XCTestCase { +@Suite("Day operations") +struct DayOperationTests { - func testPlus() { - expect((Day(daysSince1970: 19445) + 5).daysSince1970) == 19450 + @Test("+") + func plus() { + #expect((Day(daysSince1970: 19445) + 5).daysSince1970 == 19450) } - func testMinus() { - expect((Day(daysSince1970: 19445) - 5).daysSince1970) == 19440 + @Test("-") + func minus() { + #expect((Day(daysSince1970: 19445 - 5).daysSince1970) == 19440) } - func testInplacePlus() { + @Test("+=") + func inplacePlus() { var day = Day(daysSince1970: 19445) day += 5 - expect(day.daysSince1970) == 19450 + #expect(day.daysSince1970 == 19450) } - func testInplaceMinus() { + @Test("-=") + func inplaceMinus() { var day = Day(daysSince1970: 19445) day -= 5 - expect(day.daysSince1970) == 19440 + #expect(day.daysSince1970 == 19440) } - func testDayDiff() { - expect(Day(2020, 3, 12) - Day(2020, 3, 6)) == 6 + @Test("Diff") + func dayDiff() { + #expect(Day(2020, 3, 12) - Day(2020, 3, 6) == 6) } } diff --git a/Tests/DayStrideableTests.swift b/Tests/DayStrideableTests.swift index a59bf9a..02f83be 100644 --- a/Tests/DayStrideableTests.swift +++ b/Tests/DayStrideableTests.swift @@ -1,35 +1,38 @@ -import Foundation import DayType -import XCTest -import Nimble +import Foundation +import Testing -class DayStrideableTests: XCTestCase { +@Suite("Day Stridable tests") +struct DayStrideableTests { + @Test("In a for loop with an open range") func testForEachOpenRange() { var days: [Day] = [] - for day in Day(2000,1,1)...Day(2000,1,5) { + for day in Day(2000, 1, 1) ... Day(2000, 1, 5) { days.append(day) } - expect(days) == [Day(2000,1,1), Day(2000,1,2), Day(2000,1,3), Day(2000,1,4), Day(2000,1,5)] + #expect(days == [Day(2000, 1, 1), Day(2000, 1, 2), Day(2000, 1, 3), Day(2000, 1, 4), Day(2000, 1, 5)]) } + @Test("In a for loop with a half open range") func testForEachHalfOpenRange() { var days: [Day] = [] - for day in Day(2000,1,1).. 0 + #expect(Day().daysSince1970 > 0) } - func testInitDaysSince1970() { - expect(Day(daysSince1970: 19455).daysSince1970) == 19455 + @Test("Init with days since 1970") + func initDaysSince1970() { + #expect(Day(daysSince1970: 19455).daysSince1970 == 19455) } - func testInitTimeIntervalSince1970() { - expect(Day(timeIntervalSince1970: 1_680_954_742).daysSince1970) == 19455 + @Test("Init with time interval since 1970") + func initTimeIntervalSince1970() { + #expect(Day(timeIntervalSince1970: 1_680_954_742).daysSince1970 == 19455) } - func testInitTimeIntervalSince1970TruncationCheck() { - expect(Day(timeIntervalSince1970: 24 * 60 * 60 - 1).daysSince1970) == 0 - expect(Day(timeIntervalSince1970: 24 * 60 * 60 + 0).daysSince1970) == 1 - expect(Day(timeIntervalSince1970: 24 * 60 * 60 + 1).daysSince1970) == 1 + @Test("Init with time interval since 1970 time truncations") + func initTimeIntervalSince1970TruncationCheck() { + #expect(Day(timeIntervalSince1970: 24 * 60 * 60 - 1).daysSince1970 == 0) + #expect(Day(timeIntervalSince1970: 24 * 60 * 60 + 0).daysSince1970 == 1) + #expect(Day(timeIntervalSince1970: 24 * 60 * 60 + 1).daysSince1970 == 1) } - func testInitComponents() { - expect(Day(components: DayComponents(year: 2023, month: 4, day: 8)).daysSince1970) == 19455 + @Test("Init with components") + func initComponents() { + #expect(Day(components: DayComponents(year: 2023, month: 4, day: 8)).daysSince1970 == 19455) } - func testInitShortForm() { - expect(Day(2023, 4, 8).daysSince1970) == 19455 + @Test("Init with short form components") + func initShortForm() { + #expect(Day(2023, 4, 8).daysSince1970 == 19455) } - func testDateToDayToDayComponents() { + @Test("Check Day vs Date math") + func dateToDayToDayComponents() { let baseDate = Calendar.current.date(from: DateComponents(year: 1900, month: 1, day: 1))! for offset in 0 ..< 1_000_000 { let expectedDate = Calendar.current.date(byAdding: .day, value: offset, to: baseDate)! @@ -44,7 +51,7 @@ class DayTests: XCTestCase { } } - private func matches(day: Day, date: Date, file: StaticString = #file, line: UInt = #line) -> Bool { + private func matches(day: Day, date: Date, sourceLocation: Testing.SourceLocation = #_sourceLocation) -> Bool { let dayComponents = day.dayComponents() let dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date) @@ -54,7 +61,7 @@ class DayTests: XCTestCase { dayComponents.day == dateComponents.day else { print("Date components: \(dateComponents)") print("Day components : \(dayComponents)") - fail("Day from date and back to date failed", file: file, line: line) + Issue.record("Day from date and back to date failed", sourceLocation: sourceLocation) return false } return true diff --git a/Tests/Property wrappers/CustomISO8601Tests.swift b/Tests/Property wrappers/CustomISO8601Tests.swift index 7606683..0a585b9 100644 --- a/Tests/Property wrappers/CustomISO8601Tests.swift +++ b/Tests/Property wrappers/CustomISO8601Tests.swift @@ -1,8 +1,6 @@ import DayType -import Nimble -import XCTest - -// MARK: - ISO8601 Decoding +import Foundation +import Testing private struct ISO8601CustomContainer: Codable where Configurator: CustomISO8601Configurator { @CustomISO8601 var d1: Day @@ -18,87 +16,90 @@ private struct ISO8601CustomOptionalContainer: Codable where Confi } } -class CustomISO8601Tests: XCTestCase { +extension PropertyWrapperSuites { + + @Suite("@CustomISO8601") + struct CustomISO8601Tests { - func testDecodingSansTimeZone() throws { - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } + @Test("Sans Timezone decoding") + func sansTimeZoneDecoding() throws { + let json = #"{"d1": "2012-02-02T13:33:23"}"# + let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) + } - func testDecodingSansTimeZoneToTimeZone() throws { - enum SansTimeZoneToMelbourneTimeZone: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - formatter.formatOptions.remove(.withTimeZone) + @Test("Local timezone decoding") + func sansTimeZoneToLocalTimeZoneDecoding() throws { + enum SansTimeZoneToMelbourneTimeZone: CustomISO8601Configurator { + static func configure(formatter: ISO8601DateFormatter) { + formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) + formatter.formatOptions.remove(.withTimeZone) + } } + let json = #"{"d1": "2012-02-02T13:33:23"}"# + let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 02)) } - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 02) - } - func testDecodingWithTimeZoneOverridesDateTimeZone() throws { - enum BrazilToMelbourneTimeZone: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) + @Test("Floating to a different timezone decoding") + func timeZoneOverridingDateTimeZoneDecoding() throws { + enum BrazilToMelbourneTimeZone: CustomISO8601Configurator { + static func configure(formatter: ISO8601DateFormatter) { + formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) + } } + let json = #"{"d1": "2012-02-02T13:33:23-03:00"}"# + let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - let json = #"{"d1": "2012-02-02T13:33:23-03:00"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } - func testDecodingMinimalFormat() throws { - enum MinimalFormat: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) - formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) + @Test("Numeric iSO8601 decoding") + func minimalFormatDecoding() throws { + enum MinimalFormat: CustomISO8601Configurator { + static func configure(formatter: ISO8601DateFormatter) { + formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) + formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) + formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) + } } + let json = #"{"d1": "20120202 133323"}"# + let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 02)) } - let json = #"{"d1": "20120202 133323"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 02) - } - -} - -class CustomISO8601OptionalDayDecodingTests: XCTestCase { - - func testDecodingSansTimeZone() throws { - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } - func testDecodingSansTimeZoneWithNil() throws { - let json = #"{"d1":null}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1).to(beNil()) - } -} - -class CustomISO8601EncodingTests: XCTestCase { + @Test("Optional sans timezone decoding") + func optionalSansTimeZoneDecoding() throws { + let json = #"{"d1": "2012-02-02T13:33:23"}"# + let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) + } - func testEncoding() throws { - let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"2012-02-02T13:00:00"}"# - } -} + @Test("Sans timezone decoding of nil") + func optionalSansTimeZoneWithNilDecoding() throws { + let json = #"{"d1":null}"# + let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == nil) + } -class CustomISO8601OptionalDayEncodingTests: XCTestCase { + @Test("Custom ISO8601 encoding") + func encodingISO8601() throws { + let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) + } - func testEncoding() throws { - let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"2012-02-02T13:00:00"}"# - } + @Test("Optional Custom ISO8601 encoding") + func encodingToOptional() throws { + let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) + } - func testEncodingNil() throws { - let instance = ISO8601CustomOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":null}"# + @Test("Optional Custom ISO8601 encoding of nil") + func testEncodingNil() throws { + let instance = ISO8601CustomOptionalContainer(d1: nil) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) + } } } diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index e6472ac..58b5202 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -10,99 +10,99 @@ private struct OptionalDayContainer: Codable where Configurator: D @DateString var d1: Day? } -@Suite("Date string decoding") -struct DateStringDecodingTests { +extension PropertyWrapperSuites { - @Test("Decoding an ISO8601 date") - func decodingAnISO8601Date() throws { - let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Suite("@DateString") + struct DateStringTests { - @Test("Decoding an DMY date") - func decodingADMYDate() throws { - let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Test("ISO8601 date decoding") + func decodingAnISO8601Date() throws { + let json = #"{"d1": "2012-02-01"}"# + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding an MDY date") - func decodingAMDYDate() throws { - let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Test("DMY date decoding") + func decodingADMYDate() throws { + let json = #"{"d1": "01/02/2012"}"# + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding an optional ISO8601 date") - func decodingAnIOptionalISO8601Date() throws { - let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Test("MDY date decoding") + func decodingAMDYDate() throws { + let json = #"{"d1": "02/01/2012"}"# + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding an DMY date") - func decodingAnOptionalDMYDate() throws { - let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Test("Optional ISO8601 date decoding") + func decodingAnIOptionalISO8601Date() throws { + let json = #"{"d1": "2012-02-01"}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding an MDY date") - func decodingAnOptionalMDYDate() throws { - let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } + @Test("DMY date") + func decodingAnOptionalDMYDate() throws { + let json = #"{"d1": "01/02/2012"}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding a nil date") - func decodingANilDate() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } + @Test("MDY date decoding") + func decodingAnOptionalMDYDate() throws { + let json = #"{"d1": "02/01/2012"}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 01)) + } - @Test("Decoding an invalid date throws an error") - func decodingInvalidDateThrows() throws { - do { - let json = #"{"d1": "xxx"}"# - _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - } catch DecodingError.dataCorrupted(let context) { - #expect(context.codingPath.map(\.stringValue) == ["d1"]) - #expect(context.debugDescription == "Unable to read the date string.") - } catch { - Issue.record("Unexpected error: \(error)") + @Test("Nil date decoding") + func decodingANilDate() throws { + let json = #"{"d1": null}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == nil) } - } -} -@Suite("Date string encoding") -class DateStringEncodingTests { + @Test("Invalid date decoding throws an error") + func decodingInvalidDateThrows() throws { + do { + let json = #"{"d1": "xxx"}"# + _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.map(\.stringValue) == ["d1"]) + #expect(context.debugDescription == "Unable to read the date string.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } - @Test("Encoding a DMY date") - func dateEncoding() throws { - let instance = DayContainer(d1: Day(2012, 02, 01)) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) - } + @Test("DMY date encoding") + func encodingDateString() throws { + let instance = DayContainer(d1: Day(2012, 02, 01)) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let result = try encoder.encode(instance) + #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) + } - @Test("Encoding an optional DMY date") - func optionalDateEncoding() throws { - let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) - } + @Test("Optional DMY date encoding") + func encodingOptionalDateString() throws { + let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let result = try encoder.encode(instance) + #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) + } - @Test("Encoding an optional DMY date from a nil") - func optionalDateEncodingWithNil() throws { - let instance = OptionalDayContainer(d1: nil) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) + @Test("Optional DMY date encoding from a nil") + func encodingOptionalDateStringWithNil() throws { + let instance = OptionalDayContainer(d1: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let result = try encoder.encode(instance) + #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) + } } } diff --git a/Tests/Property wrappers/EpochMillisecondsTests.swift b/Tests/Property wrappers/EpochMillisecondsTests.swift index 616ecb2..471e698 100644 --- a/Tests/Property wrappers/EpochMillisecondsTests.swift +++ b/Tests/Property wrappers/EpochMillisecondsTests.swift @@ -1,6 +1,6 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing private struct EpochContainer: Codable { @EpochMilliseconds var d1: Day @@ -16,72 +16,78 @@ private struct EpochOptionalContainer: Codable { } } -class EpochMillisecondsDecodingTests: XCTestCase { +extension PropertyWrapperSuites { - func testDecoding() throws { - let json = #"{"d1": 1328251182123}"# - let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } + @Suite("@EpochMilliseconds") + struct EpochMillisecondsTests { - func testDecodingWithInvalidValue() throws { - do { - let json = #"{"d1": "2012-02-03T10:33:23.123+11:00"}"# - _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - expect(context.codingPath.last?.stringValue) == "d1" - expect(context.debugDescription) == "Unable to read a Day value, expected an epoch." + @Test("Decoding") + func decoding() throws { + let json = #"{"d1": 1328251182123}"# + let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } - -} - -class CodableAsEpochMillisecondsDecodiongOptionalTests: XCTestCase { - func testDecoding() throws { - let json = #"{"d1": 1328251182123}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } - - func testDecodingWithNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1).to(beNil()) - } + @Test("Decoding an invalid value") + func decodingInvalidValue() throws { + do { + let json = #"{"d1": "2012-02-03T10:33:23.123+11:00"}"# + _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.last?.stringValue == "d1") + #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } - func testDecodingWithMissingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - expect(key.stringValue) == "d1" + @Test("Optional decoding") + func optionalDecoding() throws { + let json = #"{"d1": 1328251182123}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } -} -class CodableAsEpochMillisecondsEncodingTests: XCTestCase { + @Test("Optional decoding nil") + func decodingNil() throws { + let json = #"{"d1": null}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == nil) + } - func testEncoding() throws { - let instance = EpochContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":1328187600000}"# - } -} + @Test("Optional decoding with a missing value") + func decodingWithMissingValue() throws { + do { + let json = #"{}"# + _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.keyNotFound(let key, _) { + #expect(key.stringValue == "d1") + } catch { + Issue.record("Unexpected error: \(error)") + } + } -class CodableAsEpochMillisecondsEncodingOptionalTests: XCTestCase { + @Test("Encoding") + func encoding() throws { + let instance = EpochContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) + } - func testEncoding() throws { - let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":1328187600000}"# - } + @Test("Optional encoding") + func encodingOptional() throws { + let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) + } - func testEncodingNil() throws { - let instance = EpochOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":null}"# + @Test("Optional encoding with a nil") + func encodingNil() throws { + let instance = EpochOptionalContainer(d1: nil) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) + } } } diff --git a/Tests/Property wrappers/EpochSecondsTests.swift b/Tests/Property wrappers/EpochSecondsTests.swift index 7755e08..60e5fb0 100644 --- a/Tests/Property wrappers/EpochSecondsTests.swift +++ b/Tests/Property wrappers/EpochSecondsTests.swift @@ -1,6 +1,6 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing private struct EpochContainer: Codable { @EpochSeconds var d1: Day @@ -16,72 +16,78 @@ private struct EpochOptionalContainer: Codable { } } -class EpochSecondsDecodingTests: XCTestCase { +extension PropertyWrapperSuites { - func testDecoding() throws { - let json = #"{"d1": 1328251182}"# - let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } + @Suite("@EpochSeconds") + struct EpochSecondsTests { - func testDecodingWithInvalidValue() throws { - do { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - expect(context.codingPath.last?.stringValue) == "d1" - expect(context.debugDescription) == "Unable to read a Day value, expected an epoch." + @Test("Decoding") + func decoding() throws { + let json = #"{"d1": 1328251182}"# + let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } - -} - -class CodableAsEpochSecondsDecodiongOptionalTests: XCTestCase { - func testDecoding() throws { - let json = #"{"d1": 1328251182}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } - - func testDecodingWithNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1).to(beNil()) - } + @Test("Decoding an invalid value") + func decodingWithInvalidValue() throws { + do { + let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# + _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.last?.stringValue == "d1") + #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } - func testDecodingWithMissingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - expect(key.stringValue) == "d1" + @Test("Decoding an optional") + func decodingOptional() throws { + let json = #"{"d1": 1328251182}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } -} -class CodableAsEpochSecondsEncodingTests: XCTestCase { + @Test("Decoding an optional with a nil") + func decodingWithNil() throws { + let json = #"{"d1": null}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == nil) + } - func testEncoding() throws { - let instance = EpochContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":1328187600}"# - } -} + @Test("Decoding an optional when value is missing") + func missingValue() throws { + do { + let json = #"{}"# + _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.keyNotFound(let key, _) { + #expect(key.stringValue == "d1") + } catch { + Issue.record("Unexpected error: \(error)") + } + } -class CodableAsEpochSecondsEncodingOptionalTests: XCTestCase { + @Test("Encoding epoch seconds") + func encoding() throws { + let instance = EpochContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) + } - func testEncoding() throws { - let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":1328187600}"# - } + @Test("Encoding when optional") + func encodingOptional() throws { + let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) + } - func testEncodingNil() throws { - let instance = EpochOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":null}"# + @Test("Encoding when ooptional and nil value") + func encodingOptionalWithNil() throws { + let instance = EpochOptionalContainer(d1: nil) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) + } } } diff --git a/Tests/Property wrappers/ISO8601Tests.swift b/Tests/Property wrappers/ISO8601Tests.swift index 090cf2b..2a9e503 100644 --- a/Tests/Property wrappers/ISO8601Tests.swift +++ b/Tests/Property wrappers/ISO8601Tests.swift @@ -1,6 +1,6 @@ import DayType -import Nimble -import XCTest +import Foundation +import Testing private struct ISO8601Container: Codable { @@ -17,77 +17,85 @@ private struct ISO8601OptionalContainer: Codable { } } -class ISO8601Tests: XCTestCase { +extension PropertyWrapperSuites { - func testDecoding() throws { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } - - func testDecodingWithDefaultGMT() throws { - let json = #"{"d1": "2012-02-02T13:33:23Z"}"# - let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } + @Suite("@ISO8601") + struct ISO8601Tests { - func testDecodingWithInvalidStringDate() throws { - do { - let json = #"{"d1": "xxxx"}"# - _ = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - expect(context.debugDescription) == "Unable to read a Day value, expected a valid ISO8601 string." - expect(context.codingPath.last?.stringValue) == "d1" + @Test("Decoding") + func decoding() throws { + let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# + let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } -} -class ISO8601OptionalTests: XCTestCase { - - func testDecoding() throws { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1) == Day(2012, 02, 03) - } + @Test("Decoding a GMT value") + func decodingWithDefaultGMT() throws { + let json = #"{"d1": "2012-02-02T13:33:23Z"}"# + let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) + } - func testDecodingWithNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - expect(result.d1).to(beNil()) - } + @Test("Decoding invalid value") + func decodingWithInvalidStringDate() throws { + do { + let json = #"{"d1": "xxxx"}"# + _ = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.debugDescription == "Unable to read a Day value, expected a valid ISO8601 string.") + #expect(context.codingPath.last?.stringValue == "d1") + } catch { + Issue.record("Unexpected error: \(error)") + } + } - func testDecodingWithMissingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - fail("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - expect(key.stringValue) == "d1" + @Test("Decoding optional") + func decodingOptional() throws { + let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# + let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == Day(2012, 02, 03)) } - } -} -class ISO8601DayEncodingTests: XCTestCase { + @Test("Decoding optional with nil") + func decodingOptionalWithNil() throws { + let json = #"{"d1": null}"# + let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.d1 == nil) + } - func testEncoding() throws { - let instance = ISO8601Container(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"2012-02-02T13:00:00Z"}"# - } -} + @Test("Decoding optional with missing value") + func decodingOptionalWithMissingValue() throws { + do { + let json = #"{}"# + _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.keyNotFound(let key, _) { + #expect(key.stringValue == "d1") + } catch { + Issue.record("Unexpected error: \(error)") + } + } -class ISO8601OptionalDayEncodingTests: XCTestCase { + @Test("Encoding") + func encoding() throws { + let instance = ISO8601Container(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) + } - func testEncoding() throws { - let instance = ISO8601OptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":"2012-02-02T13:00:00Z"}"# - } + @Test("Encoding optional") + func encodingOptional() throws { + let instance = ISO8601OptionalContainer(d1: Day(2012, 02, 03)) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) + } - func testEncodingNil() throws { - let instance = ISO8601OptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - expect(String(data: result, encoding: .utf8)!) == #"{"d1":null}"# + @Test("Encoding optional with nil") + func encodingOptionalNil() throws { + let instance = ISO8601OptionalContainer(d1: nil) + let result = try JSONEncoder().encode(instance) + #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) + } } } diff --git a/Tests/Property wrappers/PropertyWrappers.swift b/Tests/Property wrappers/PropertyWrappers.swift new file mode 100644 index 0000000..6e676a2 --- /dev/null +++ b/Tests/Property wrappers/PropertyWrappers.swift @@ -0,0 +1,6 @@ +import Testing + +/// Groups up property test suites. +@Suite("Property wrappers", .tags(.PropertyWrapper)) +struct PropertyWrapperSuites {} + From feff7a2771e401e389174a33b53bf72cf867025c Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 20 Nov 2024 16:23:13 +1100 Subject: [PATCH 15/26] Moving protocol conformance and fixing tests --- Sources/{ => Conformance}/Day+Codable.swift | 0 Sources/Conformance/Day+Comparable.swift | 7 ++++ .../Day+CustomStringConvertable.swift | 2 +- Sources/Conformance/Day+Equatable.swift | 7 ++++ Sources/Conformance/Day+Hashable.swift | 7 ++++ .../Day+Stridable.swift} | 4 +- Sources/Day+Core.swift | 19 --------- Tests/Conformance/DayCodableTests.swift | 36 ++++++++++++++++ .../DayConparableTests.swift} | 27 +----------- .../DayCustomStringConvertableTests.swift | 12 ++++++ Tests/Conformance/DayEquatableTests.swift | 12 ++++++ Tests/Conformance/DayHashableTests.swift | 24 +++++++++++ Tests/Conformance/DayStrideableTests.swift | 42 +++++++++++++++++++ .../ProtocolConformanceSuites.swift | 6 +++ Tests/DayCodableTests.swift | 33 --------------- Tests/DayCustomStringConvertableTests.swift | 9 ---- Tests/DayOperationsTests.swift | 2 +- Tests/DayStrideableTests.swift | 38 ----------------- ...pers.swift => PropertyWrapperSuites.swift} | 0 Tests/Tags.swift | 1 + 20 files changed, 159 insertions(+), 129 deletions(-) rename Sources/{ => Conformance}/Day+Codable.swift (100%) create mode 100644 Sources/Conformance/Day+Comparable.swift rename Sources/{ => Conformance}/Day+CustomStringConvertable.swift (80%) create mode 100644 Sources/Conformance/Day+Equatable.swift create mode 100644 Sources/Conformance/Day+Hashable.swift rename Sources/{Day+Strideable.swift => Conformance/Day+Stridable.swift} (76%) delete mode 100644 Sources/Day+Core.swift create mode 100644 Tests/Conformance/DayCodableTests.swift rename Tests/{DayCoreTests.swift => Conformance/DayConparableTests.swift} (60%) create mode 100644 Tests/Conformance/DayCustomStringConvertableTests.swift create mode 100644 Tests/Conformance/DayEquatableTests.swift create mode 100644 Tests/Conformance/DayHashableTests.swift create mode 100644 Tests/Conformance/DayStrideableTests.swift create mode 100644 Tests/Conformance/ProtocolConformanceSuites.swift delete mode 100644 Tests/DayCodableTests.swift delete mode 100644 Tests/DayCustomStringConvertableTests.swift delete mode 100644 Tests/DayStrideableTests.swift rename Tests/Property wrappers/{PropertyWrappers.swift => PropertyWrapperSuites.swift} (100%) diff --git a/Sources/Day+Codable.swift b/Sources/Conformance/Day+Codable.swift similarity index 100% rename from Sources/Day+Codable.swift rename to Sources/Conformance/Day+Codable.swift diff --git a/Sources/Conformance/Day+Comparable.swift b/Sources/Conformance/Day+Comparable.swift new file mode 100644 index 0000000..4b37164 --- /dev/null +++ b/Sources/Conformance/Day+Comparable.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Day: Comparable { + public static func < (lhs: Day, rhs: Day) -> Bool { + lhs.daysSince1970 < rhs.daysSince1970 + } +} diff --git a/Sources/Day+CustomStringConvertable.swift b/Sources/Conformance/Day+CustomStringConvertable.swift similarity index 80% rename from Sources/Day+CustomStringConvertable.swift rename to Sources/Conformance/Day+CustomStringConvertable.swift index 948e0e5..232e297 100644 --- a/Sources/Day+CustomStringConvertable.swift +++ b/Sources/Conformance/Day+CustomStringConvertable.swift @@ -2,6 +2,6 @@ import Foundation extension Day: CustomStringConvertible { public var description: String { - self.formatted() + formatted() } } diff --git a/Sources/Conformance/Day+Equatable.swift b/Sources/Conformance/Day+Equatable.swift new file mode 100644 index 0000000..8000622 --- /dev/null +++ b/Sources/Conformance/Day+Equatable.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Day: Equatable { + public static func == (lhs: Day, rhs: Day) -> Bool { + lhs.daysSince1970 == rhs.daysSince1970 + } +} diff --git a/Sources/Conformance/Day+Hashable.swift b/Sources/Conformance/Day+Hashable.swift new file mode 100644 index 0000000..013a939 --- /dev/null +++ b/Sources/Conformance/Day+Hashable.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Day: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(daysSince1970) + } +} diff --git a/Sources/Day+Strideable.swift b/Sources/Conformance/Day+Stridable.swift similarity index 76% rename from Sources/Day+Strideable.swift rename to Sources/Conformance/Day+Stridable.swift index e70ba41..562939a 100644 --- a/Sources/Day+Strideable.swift +++ b/Sources/Conformance/Day+Stridable.swift @@ -3,9 +3,9 @@ import Foundation extension Day: Strideable { public func distance(to other: Day) -> Int { - other.daysSince1970 - self.daysSince1970 + other.daysSince1970 - daysSince1970 } - + public func advanced(by n: Int) -> Day { self + n } diff --git a/Sources/Day+Core.swift b/Sources/Day+Core.swift deleted file mode 100644 index b4fc872..0000000 --- a/Sources/Day+Core.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -extension Day: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(daysSince1970) - } -} - -extension Day: Equatable { - public static func == (lhs: Day, rhs: Day) -> Bool { - lhs.daysSince1970 == rhs.daysSince1970 - } -} - -extension Day: Comparable { - public static func < (lhs: Day, rhs: Day) -> Bool { - lhs.daysSince1970 < rhs.daysSince1970 - } -} diff --git a/Tests/Conformance/DayCodableTests.swift b/Tests/Conformance/DayCodableTests.swift new file mode 100644 index 0000000..0c2db6b --- /dev/null +++ b/Tests/Conformance/DayCodableTests.swift @@ -0,0 +1,36 @@ +import DayType +import Foundation +import Testing + +extension ProtocolConformanceSuites { + + @Suite("Day is Codable") + struct DayCodableTests { + + private struct DummyType: Codable { + let abc: Day + } + + @Test("Decoding") + func decoding() throws { + + let json = """ + { + "abc": 19455 + } + """ + + let decoder = JSONDecoder() + let day = try decoder.decode(DummyType.self, from: json.data(using: .utf8)!) + #expect(day.abc.daysSince1970 == 19455) + } + + @Test("Encoding") + func encoding() throws { + let obj = DummyType(abc: Day(daysSince1970: 19455)) + let encoder = JSONEncoder() + let encoded = try #require(String(data: encoder.encode(obj), encoding: .utf8)) + #expect(encoded == #"{"abc":19455}"#) + } + } +} diff --git a/Tests/DayCoreTests.swift b/Tests/Conformance/DayConparableTests.swift similarity index 60% rename from Tests/DayCoreTests.swift rename to Tests/Conformance/DayConparableTests.swift index 0918117..e1710da 100644 --- a/Tests/DayCoreTests.swift +++ b/Tests/Conformance/DayConparableTests.swift @@ -2,32 +2,7 @@ import DayType import Foundation import Testing -@Suite("Protocol conformance") -struct DayProtocols { - - @Test("Hash") - func hash() { - var days: Set = [Day(2020, 01, 11), Day(2020, 01, 12)] - - #expect(days.contains(Day(2020, 01, 13)) == false) - #expect(days.contains(Day(2020, 01, 12)) == true) - - // Modify and try again. - days.insert(Day(2020, 01, 13)) - #expect(days.count == 3) - #expect(days.contains(Day(2020, 01, 13)) == true) - #expect(days.contains(Day(2020, 01, 12)) == true) - - // Duplicate check. - days.insert(Day(2020, 01, 11)) - #expect(days.count == 3) - } - - @Test("Equatble") - func equals() { - #expect(Day(2020, 3, 12) == Day(2020, 3, 12)) - #expect(Day(2020, 3, 12) != Day(2001, 1, 5)) - } +extension ProtocolConformanceSuites { @Suite("Comparable") struct DayComparableTests { diff --git a/Tests/Conformance/DayCustomStringConvertableTests.swift b/Tests/Conformance/DayCustomStringConvertableTests.swift new file mode 100644 index 0000000..54f7f8a --- /dev/null +++ b/Tests/Conformance/DayCustomStringConvertableTests.swift @@ -0,0 +1,12 @@ +import DayType +import Foundation +import Testing + +extension ProtocolConformanceSuites { + + @Test("Day custom string convertable") + func description() { + let date = DateComponents(calendar: .current, year: 2001, month: 2, day: 3).date! + #expect(Day(2001, 2, 3).description == date.formatted(date: .abbreviated, time: .omitted)) + } +} diff --git a/Tests/Conformance/DayEquatableTests.swift b/Tests/Conformance/DayEquatableTests.swift new file mode 100644 index 0000000..2ba569c --- /dev/null +++ b/Tests/Conformance/DayEquatableTests.swift @@ -0,0 +1,12 @@ +import DayType +import Foundation +import Testing + +extension ProtocolConformanceSuites { + + @Test("Equatable") + func equals() { + #expect(Day(2020, 3, 12) == Day(2020, 3, 12)) + #expect(Day(2020, 3, 12) != Day(2001, 1, 5)) + } +} diff --git a/Tests/Conformance/DayHashableTests.swift b/Tests/Conformance/DayHashableTests.swift new file mode 100644 index 0000000..5f4ff60 --- /dev/null +++ b/Tests/Conformance/DayHashableTests.swift @@ -0,0 +1,24 @@ +import DayType +import Foundation +import Testing + +extension ProtocolConformanceSuites { + + @Test("Hashable") + func hashable() { + var days: Set = [Day(2020, 01, 11), Day(2020, 01, 12)] + + #expect(days.contains(Day(2020, 01, 13)) == false) + #expect(days.contains(Day(2020, 01, 12)) == true) + + // Modify and try again. + days.insert(Day(2020, 01, 13)) + #expect(days.count == 3) + #expect(days.contains(Day(2020, 01, 13)) == true) + #expect(days.contains(Day(2020, 01, 12)) == true) + + // Duplicate check. + days.insert(Day(2020, 01, 11)) + #expect(days.count == 3) + } +} diff --git a/Tests/Conformance/DayStrideableTests.swift b/Tests/Conformance/DayStrideableTests.swift new file mode 100644 index 0000000..0027128 --- /dev/null +++ b/Tests/Conformance/DayStrideableTests.swift @@ -0,0 +1,42 @@ +import DayType +import Foundation +import Testing + +extension ProtocolConformanceSuites { + + @Suite("Day Stridable tests") + struct DayStrideableTests { + + @Test("In a for loop with an open range") + func forEachOpenRange() { + var days: [Day] = [] + for day in Day(2000, 1, 1) ... Day(2000, 1, 5) { + days.append(day) + } + #expect(days == [Day(2000, 1, 1), Day(2000, 1, 2), Day(2000, 1, 3), Day(2000, 1, 4), Day(2000, 1, 5)]) + } + + @Test("In a for loop with a half open range") + func forEachHalfOpenRange() { + var days: [Day] = [] + for day in Day(2000, 1, 1) ..< Day(2000, 1, 5) { + days.append(day) + } + #expect(days == [Day(2000, 1, 1), Day(2000, 1, 2), Day(2000, 1, 3), Day(2000, 1, 4)]) + } + + @Test("With the stride function") + func forEachViaStrideFunction() { + var days: [Day] = [] + for day in stride(from: Day(2000, 1, 1), to: Day(2000, 1, 5), by: 2) { + days.append(day) + } + #expect(days == [Day(2000, 1, 1), Day(2000, 1, 3)]) + } + + @Test("Distance function") + func distanceTo() { + #expect(Day(2020, 3, 6).distance(to: Day(2020, 3, 12)) == 6) + } + } +} diff --git a/Tests/Conformance/ProtocolConformanceSuites.swift b/Tests/Conformance/ProtocolConformanceSuites.swift new file mode 100644 index 0000000..77b9de2 --- /dev/null +++ b/Tests/Conformance/ProtocolConformanceSuites.swift @@ -0,0 +1,6 @@ +import Testing + +/// Groups up property test suites. +@Suite("Protocol conformance", .tags(.ProtocolConformance)) +struct ProtocolConformanceSuites {} + diff --git a/Tests/DayCodableTests.swift b/Tests/DayCodableTests.swift deleted file mode 100644 index cf9e6b7..0000000 --- a/Tests/DayCodableTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -import DayType -import Foundation -import Testing - -@Suite("Day is Codable") -struct DayCodableTests { - - private struct DummyType: Codable { - let abc: Day - } - - @Test("Decoding") - func decoding() throws { - - let json = """ - { - "abc": 19455 - } - """ - - let decoder = JSONDecoder() - let day = try decoder.decode(DummyType.self, from: json.data(using: .utf8)!) - #expect(day.abc.daysSince1970 == 19455) - } - - @Test("Encoding") - func encoding() throws { - let obj = DummyType(abc: Day(daysSince1970: 19455)) - let encoder = JSONEncoder() - let encoded = try #require(String(data: encoder.encode(obj), encoding: .utf8)) - #expect(encoded == #"{"abc":19455}"#) - } -} diff --git a/Tests/DayCustomStringConvertableTests.swift b/Tests/DayCustomStringConvertableTests.swift deleted file mode 100644 index 7b0549c..0000000 --- a/Tests/DayCustomStringConvertableTests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import DayType -import Foundation -import Testing - -@Test("Day description matches Date formatted") -func description() { - let date = DateComponents(calendar: .current, year: 2001, month: 2, day: 3).date! - #expect(Day(2001, 2, 3).description == date.formatted(date: .abbreviated, time: .omitted)) -} diff --git a/Tests/DayOperationsTests.swift b/Tests/DayOperationsTests.swift index a5e032f..bb9cef7 100644 --- a/Tests/DayOperationsTests.swift +++ b/Tests/DayOperationsTests.swift @@ -12,7 +12,7 @@ struct DayOperationTests { @Test("-") func minus() { - #expect((Day(daysSince1970: 19445 - 5).daysSince1970) == 19440) + #expect((Day(daysSince1970: 19445) - 5).daysSince1970 == 19440) } @Test("+=") diff --git a/Tests/DayStrideableTests.swift b/Tests/DayStrideableTests.swift deleted file mode 100644 index 02f83be..0000000 --- a/Tests/DayStrideableTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -import DayType -import Foundation -import Testing - -@Suite("Day Stridable tests") -struct DayStrideableTests { - - @Test("In a for loop with an open range") - func testForEachOpenRange() { - var days: [Day] = [] - for day in Day(2000, 1, 1) ... Day(2000, 1, 5) { - days.append(day) - } - #expect(days == [Day(2000, 1, 1), Day(2000, 1, 2), Day(2000, 1, 3), Day(2000, 1, 4), Day(2000, 1, 5)]) - } - - @Test("In a for loop with a half open range") - func testForEachHalfOpenRange() { - var days: [Day] = [] - for day in Day(2000, 1, 1) ..< Day(2000, 1, 5) { - days.append(day) - } - #expect(days == [Day(2000, 1, 1), Day(2000, 1, 2), Day(2000, 1, 3), Day(2000, 1, 4)]) - } - - @Test("With the stride function") - func testForEachViaStrideFunction() { - var days: [Day] = [] - for day in stride(from: Day(2000, 1, 1), to: Day(2000, 1, 5), by: 2) { - days.append(day) - } - #expect(days == [Day(2000, 1, 1), Day(2000, 1, 3)]) - } - - func testDistanceTo() { - #expect(Day(2020, 3, 6).distance(to: Day(2020, 3, 12)) == 6) - } -} diff --git a/Tests/Property wrappers/PropertyWrappers.swift b/Tests/Property wrappers/PropertyWrapperSuites.swift similarity index 100% rename from Tests/Property wrappers/PropertyWrappers.swift rename to Tests/Property wrappers/PropertyWrapperSuites.swift diff --git a/Tests/Tags.swift b/Tests/Tags.swift index b23ed3a..a64dba0 100644 --- a/Tests/Tags.swift +++ b/Tests/Tags.swift @@ -2,5 +2,6 @@ import Testing extension Tag { @Tag static var PropertyWrapper: Self + @Tag static var ProtocolConformance: Self } From 5610fbcc2ab44dc66c929d970bfd6052897bff48 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 20 Nov 2024 16:24:38 +1100 Subject: [PATCH 16/26] Update swift.yml --- .github/workflows/swift.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 110668a..e1dc34f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -24,4 +24,5 @@ jobs: - run: swift test --enable-code-coverage - uses: drekka/swift-coverage-action@v1.3 with: + coverage: 90 show-all-files: true From eceef57074835ebc2ba5ec3a3d5ae50d8d4ed17a Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Thu, 21 Nov 2024 10:31:41 +1100 Subject: [PATCH 17/26] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e1dc34f..7c50de6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,7 +22,7 @@ jobs: timezoneMacos: "Australia/Melbourne" timezoneWindows: "Australia/Melbourne" - run: swift test --enable-code-coverage - - uses: drekka/swift-coverage-action@v1.3 + - uses: drekka/swift-coverage-action@develop with: coverage: 90 show-all-files: true From 579f9e316cb0039aff41bfff1ba02771c8676ead Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sat, 23 Nov 2024 13:17:14 +1100 Subject: [PATCH 18/26] Replacing epoch factor with boolean. --- .../Property wrappers/Day+EpochCodable.swift | 30 ++++++++++++------- .../Property wrappers/EpochMilliseconds.swift | 4 +-- Sources/Property wrappers/EpochSeconds.swift | 4 +-- Tests/Conformance/DayCodableTests.swift | 2 +- .../DayCustomStringConvertableTests.swift | 2 +- Tests/Conformance/DayStrideableTests.swift | 2 +- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Sources/Property wrappers/Day+EpochCodable.swift b/Sources/Property wrappers/Day+EpochCodable.swift index 776e343..8e15c1e 100644 --- a/Sources/Property wrappers/Day+EpochCodable.swift +++ b/Sources/Property wrappers/Day+EpochCodable.swift @@ -5,38 +5,48 @@ import Foundation /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol EpochCodable { - init(epochDecoder decoder: Decoder, factor: Double) throws - func encode(epochEncoder encoder: Encoder, factor: Double) throws + + /// Decoding with optional milliseconds. + init(epochDecoder decoder: Decoder, withMilliseconds: Bool) throws + + /// Encoding with optional milliseconds. + func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws +} + +/// Providing defaults. +public extension EpochCodable { + init(epochDecoder decoder: Decoder) throws { try self.init(epochDecoder: decoder, withMilliseconds: false) } + func encode(epochEncoder encoder: Encoder) throws { try encode(epochEncoder: encoder, withMilliseconds: false) } } extension Day: EpochCodable { - public init(epochDecoder decoder: Decoder, factor: Double) throws { + public init(epochDecoder decoder: Decoder, withMilliseconds milliseconds: Bool) throws { let container = try decoder.singleValueContainer() guard let epochTime = try? container.decode(TimeInterval.self) else { let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read a Day value, expected an epoch.") throw DecodingError.dataCorrupted(context) } - self = Day(date: Date(timeIntervalSince1970: epochTime / factor)) + self = Day(date: Date(timeIntervalSince1970: epochTime / (milliseconds ? 1000 : 1))) } - public func encode(epochEncoder encoder: Encoder, factor: Double) throws { + public func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws { var container = encoder.singleValueContainer() - try container.encode(date().timeIntervalSince1970 * factor) + try container.encode(date().timeIntervalSince1970 * (milliseconds ? 1000 : 1)) } } /// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. extension Day?: EpochCodable { - public init(epochDecoder decoder: Decoder, factor: Double) throws { + public init(epochDecoder decoder: Decoder, withMilliseconds milliseconds: Bool) throws { let container = try decoder.singleValueContainer() - self = container.decodeNil() ? nil : try Day(epochDecoder: decoder, factor: factor) + self = container.decodeNil() ? nil : try Day(epochDecoder: decoder, withMilliseconds: milliseconds) } - public func encode(epochEncoder encoder: Encoder, factor: Double) throws { + public func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws { if let self { - try self.encode(epochEncoder: encoder, factor: factor) + try self.encode(epochEncoder: encoder, withMilliseconds: milliseconds) } else { var container = encoder.singleValueContainer() try container.encodeNil() diff --git a/Sources/Property wrappers/EpochMilliseconds.swift b/Sources/Property wrappers/EpochMilliseconds.swift index 187a19e..495da37 100644 --- a/Sources/Property wrappers/EpochMilliseconds.swift +++ b/Sources/Property wrappers/EpochMilliseconds.swift @@ -11,10 +11,10 @@ public struct EpochMilliseconds: Codable where T: EpochCodable { } public init(from decoder: Decoder) throws { - wrappedValue = try T(epochDecoder: decoder, factor: 1000.0) + wrappedValue = try T(epochDecoder: decoder, withMilliseconds: true) } public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(epochEncoder: encoder, factor: 1000.0) + try wrappedValue.encode(epochEncoder: encoder, withMilliseconds: true) } } diff --git a/Sources/Property wrappers/EpochSeconds.swift b/Sources/Property wrappers/EpochSeconds.swift index 819a440..5029ffc 100644 --- a/Sources/Property wrappers/EpochSeconds.swift +++ b/Sources/Property wrappers/EpochSeconds.swift @@ -11,10 +11,10 @@ public struct EpochSeconds: Codable where T: EpochCodable { } public init(from decoder: Decoder) throws { - wrappedValue = try T(epochDecoder: decoder, factor: 1.0) + wrappedValue = try T(epochDecoder: decoder) } public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(epochEncoder: encoder, factor: 1.0) + try wrappedValue.encode(epochEncoder: encoder) } } diff --git a/Tests/Conformance/DayCodableTests.swift b/Tests/Conformance/DayCodableTests.swift index 0c2db6b..ae49e85 100644 --- a/Tests/Conformance/DayCodableTests.swift +++ b/Tests/Conformance/DayCodableTests.swift @@ -4,7 +4,7 @@ import Testing extension ProtocolConformanceSuites { - @Suite("Day is Codable") + @Suite("Codable") struct DayCodableTests { private struct DummyType: Codable { diff --git a/Tests/Conformance/DayCustomStringConvertableTests.swift b/Tests/Conformance/DayCustomStringConvertableTests.swift index 54f7f8a..9e6bce5 100644 --- a/Tests/Conformance/DayCustomStringConvertableTests.swift +++ b/Tests/Conformance/DayCustomStringConvertableTests.swift @@ -4,7 +4,7 @@ import Testing extension ProtocolConformanceSuites { - @Test("Day custom string convertable") + @Test("CustomStringConvertable") func description() { let date = DateComponents(calendar: .current, year: 2001, month: 2, day: 3).date! #expect(Day(2001, 2, 3).description == date.formatted(date: .abbreviated, time: .omitted)) diff --git a/Tests/Conformance/DayStrideableTests.swift b/Tests/Conformance/DayStrideableTests.swift index 0027128..0c52072 100644 --- a/Tests/Conformance/DayStrideableTests.swift +++ b/Tests/Conformance/DayStrideableTests.swift @@ -4,7 +4,7 @@ import Testing extension ProtocolConformanceSuites { - @Suite("Day Stridable tests") + @Suite("Stridable") struct DayStrideableTests { @Test("In a for loop with an open range") From 93fdb1fc9c97c20d408cd99149432c2c70779b70 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Fri, 6 Dec 2024 18:22:13 +1100 Subject: [PATCH 19/26] WiP --- README.md | 8 +++---- .../{ => DateString}/DateString.swift | 0 .../DateStringConfigurator.swift | 13 ++++++++++- .../Day+DateStringCodable.swift | 0 Tests/Property wrappers/DateStringTests.swift | 22 +++++++++---------- 5 files changed, 27 insertions(+), 16 deletions(-) rename Sources/Property wrappers/{ => DateString}/DateString.swift (100%) rename Sources/Property wrappers/{ => DateString}/DateStringConfigurator.swift (73%) rename Sources/Property wrappers/{ => DateString}/Day+DateStringCodable.swift (100%) diff --git a/README.md b/README.md index f93e3e7..ac19e6a 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ ![Calendar](media/Calendar.png) # DayType -_An API for dates that doesn't involve hours, minutes, seconds and timezones._ +_An API for dates and nothing else. No Calendars, timezones, hours, minutes or seconds. Just dates._ -Swift provides excellent date support through it's `Date`, `Calendar`, `TimeZone` and other types. However there's a catch, they're all designed to with with specific points in time rather than the generalisations people often use. +Swift provides excellent date support through it's `Date`, `Calendar`, `TimeZone` and other types. However there's a catch, they're all designed to work with specific points in time. Not the generalisations that people often use. -For example, the APIS cannot refer to a person's birthday without anchoring it to a specific hour, minute, second and even partial second within a specific timezone. Yet people when discussion a person's birthday only think of the date in whatever timezone they are in. Not the exact moment of a person's birth which sometime's even the person being discussed doesn't know.The same goes for other dates people often work with, an employee leave, religious holidays, retail sales, festivals, etc all typically have a date associated, but not a time. +For example, you cannot just refer to a person's birthday date without anchoring it to a specific time. In a specific timezone. But people don't consider that and they often don't even know. Just referring to the date in whatever timezone they are in. The same goes for a variety of other dates. Employment leave, religious holidays, retail sales, festivals, etc. All often referred to by date only. -As a result developers often find themselves writing code to strip time from Swift's `Date` in order to trick it into acting like a date. Often with mixed results as there are many technical issues to consider when coercing a point in time to such a generalisation. Especially with time zones and sometime questionable input from external sources. +As a result developers often find themselves writing code to strip time from Swift's `Date` and trying to manipulate the results to allow for timezones. Often with mixed results as there are all sorts of technical issues to consider when coercing a point in time to such a generalisation. Especially with time zones and sometime questionable input from external sources. `DayType` provides simplify date handling through it's `Day` type. A `Day` is a representation of a 24 hours period instead of a specific point in time. ie. it doesn't have any hours, minutes, timezones, etc. This allows date code to be simpler because the developer no longer needs to sanitise time components, and that removes the angst of accidental bugs as well as making date based code considerably simpler. diff --git a/Sources/Property wrappers/DateString.swift b/Sources/Property wrappers/DateString/DateString.swift similarity index 100% rename from Sources/Property wrappers/DateString.swift rename to Sources/Property wrappers/DateString/DateString.swift diff --git a/Sources/Property wrappers/DateStringConfigurator.swift b/Sources/Property wrappers/DateString/DateStringConfigurator.swift similarity index 73% rename from Sources/Property wrappers/DateStringConfigurator.swift rename to Sources/Property wrappers/DateString/DateStringConfigurator.swift index ba62fbf..086d58b 100644 --- a/Sources/Property wrappers/DateStringConfigurator.swift +++ b/Sources/Property wrappers/DateString/DateStringConfigurator.swift @@ -7,8 +7,13 @@ public protocol DateStringConfigurator { static func configure(formatter: DateFormatter) } +public extension DateStringConfigurator { + + static var ISO: DateStringConfigurator.Type { DateFormatterConfig.ISO.self } +} + /// Useful common configurations of date formatters. -public enum DateStringConfig { +public enum DateFormatterConfig { public enum ISO: DateStringConfigurator { public static func configure(formatter: DateFormatter) { @@ -28,3 +33,9 @@ public enum DateStringConfig { } } } + +struct ISO: DateStringConfigurator { + public static func configure(formatter: DateFormatter) { + formatter.dateFormat = "yyyy-MM-dd" + } +} diff --git a/Sources/Property wrappers/Day+DateStringCodable.swift b/Sources/Property wrappers/DateString/Day+DateStringCodable.swift similarity index 100% rename from Sources/Property wrappers/Day+DateStringCodable.swift rename to Sources/Property wrappers/DateString/Day+DateStringCodable.swift diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 58b5202..93e9921 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -18,49 +18,49 @@ extension PropertyWrapperSuites { @Test("ISO8601 date decoding") func decodingAnISO8601Date() throws { let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("DMY date decoding") func decodingADMYDate() throws { let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("MDY date decoding") func decodingAMDYDate() throws { let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("Optional ISO8601 date decoding") func decodingAnIOptionalISO8601Date() throws { let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("DMY date") func decodingAnOptionalDMYDate() throws { let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("MDY date decoding") func decodingAnOptionalMDYDate() throws { let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 01)) } @Test("Nil date decoding") func decodingANilDate() throws { let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == nil) } @@ -68,7 +68,7 @@ extension PropertyWrapperSuites { func decodingInvalidDateThrows() throws { do { let json = #"{"d1": "xxx"}"# - _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) Issue.record("Error not thrown") } catch DecodingError.dataCorrupted(let context) { #expect(context.codingPath.map(\.stringValue) == ["d1"]) @@ -80,7 +80,7 @@ extension PropertyWrapperSuites { @Test("DMY date encoding") func encodingDateString() throws { - let instance = DayContainer(d1: Day(2012, 02, 01)) + let instance = DayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) @@ -89,7 +89,7 @@ extension PropertyWrapperSuites { @Test("Optional DMY date encoding") func encodingOptionalDateString() throws { - let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) + let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) @@ -98,7 +98,7 @@ extension PropertyWrapperSuites { @Test("Optional DMY date encoding from a nil") func encodingOptionalDateStringWithNil() throws { - let instance = OptionalDayContainer(d1: nil) + let instance = OptionalDayContainer(d1: nil) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) From 68069e3a4ca5288bad799231ef37fc1c1a77b05f Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 8 Dec 2024 14:31:49 +1100 Subject: [PATCH 20/26] Moving files --- README.md | 4 ++-- .../{Day+DateStringCodable.swift => DateStringCodable.swift} | 0 .../{Day+EpochCodable.swift => Epoch/EpochCodable.swift} | 0 Sources/Property wrappers/{ => Epoch}/EpochMilliseconds.swift | 0 Sources/Property wrappers/{ => Epoch}/EpochSeconds.swift | 0 Sources/Property wrappers/{ => ISO8601}/CustomISO8601.swift | 0 .../{ => ISO8601}/CustomISO8601Configurator.swift | 0 Sources/Property wrappers/{ => ISO8601}/ISO8601.swift | 0 .../ISO8601Codable.swift} | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename Sources/Property wrappers/DateString/{Day+DateStringCodable.swift => DateStringCodable.swift} (100%) rename Sources/Property wrappers/{Day+EpochCodable.swift => Epoch/EpochCodable.swift} (100%) rename Sources/Property wrappers/{ => Epoch}/EpochMilliseconds.swift (100%) rename Sources/Property wrappers/{ => Epoch}/EpochSeconds.swift (100%) rename Sources/Property wrappers/{ => ISO8601}/CustomISO8601.swift (100%) rename Sources/Property wrappers/{ => ISO8601}/CustomISO8601Configurator.swift (100%) rename Sources/Property wrappers/{ => ISO8601}/ISO8601.swift (100%) rename Sources/Property wrappers/{Day+ISO8601Codable.swift => ISO8601/ISO8601Codable.swift} (100%) diff --git a/README.md b/README.md index ac19e6a..d8df2f3 100755 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Swift provides excellent date support through it's `Date`, `Calendar`, `TimeZone For example, you cannot just refer to a person's birthday date without anchoring it to a specific time. In a specific timezone. But people don't consider that and they often don't even know. Just referring to the date in whatever timezone they are in. The same goes for a variety of other dates. Employment leave, religious holidays, retail sales, festivals, etc. All often referred to by date only. -As a result developers often find themselves writing code to strip time from Swift's `Date` and trying to manipulate the results to allow for timezones. Often with mixed results as there are all sorts of technical issues to consider when coercing a point in time to such a generalisation. Especially with time zones and sometime questionable input from external sources. +As a result developers often find themselves stripping the time and timezone components from Swift's `Date`. Often with mixed results as there are a number of complexities to consider when coercing a point in time to a generalisation. Especially when considering time zones and often questionable input from external sources. -`DayType` provides simplify date handling through it's `Day` type. A `Day` is a representation of a 24 hours period instead of a specific point in time. ie. it doesn't have any hours, minutes, timezones, etc. This allows date code to be simpler because the developer no longer needs to sanitise time components, and that removes the angst of accidental bugs as well as making date based code considerably simpler. +`DayType` provides simplify date handling through a new type called `Day`. That being a representation of a 24 hours period which is indenpendant of any timezone and not anchored to a specific point in time. ie. there's no hours, minutes, timezones, etc. This allows date code to be simpler because as a developer you no longer needs to sanitise or adjust time components. which alleviates the angst of accidental bugs as well as making the code considerably simpler. ## Installation diff --git a/Sources/Property wrappers/DateString/Day+DateStringCodable.swift b/Sources/Property wrappers/DateString/DateStringCodable.swift similarity index 100% rename from Sources/Property wrappers/DateString/Day+DateStringCodable.swift rename to Sources/Property wrappers/DateString/DateStringCodable.swift diff --git a/Sources/Property wrappers/Day+EpochCodable.swift b/Sources/Property wrappers/Epoch/EpochCodable.swift similarity index 100% rename from Sources/Property wrappers/Day+EpochCodable.swift rename to Sources/Property wrappers/Epoch/EpochCodable.swift diff --git a/Sources/Property wrappers/EpochMilliseconds.swift b/Sources/Property wrappers/Epoch/EpochMilliseconds.swift similarity index 100% rename from Sources/Property wrappers/EpochMilliseconds.swift rename to Sources/Property wrappers/Epoch/EpochMilliseconds.swift diff --git a/Sources/Property wrappers/EpochSeconds.swift b/Sources/Property wrappers/Epoch/EpochSeconds.swift similarity index 100% rename from Sources/Property wrappers/EpochSeconds.swift rename to Sources/Property wrappers/Epoch/EpochSeconds.swift diff --git a/Sources/Property wrappers/CustomISO8601.swift b/Sources/Property wrappers/ISO8601/CustomISO8601.swift similarity index 100% rename from Sources/Property wrappers/CustomISO8601.swift rename to Sources/Property wrappers/ISO8601/CustomISO8601.swift diff --git a/Sources/Property wrappers/CustomISO8601Configurator.swift b/Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift similarity index 100% rename from Sources/Property wrappers/CustomISO8601Configurator.swift rename to Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift diff --git a/Sources/Property wrappers/ISO8601.swift b/Sources/Property wrappers/ISO8601/ISO8601.swift similarity index 100% rename from Sources/Property wrappers/ISO8601.swift rename to Sources/Property wrappers/ISO8601/ISO8601.swift diff --git a/Sources/Property wrappers/Day+ISO8601Codable.swift b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift similarity index 100% rename from Sources/Property wrappers/Day+ISO8601Codable.swift rename to Sources/Property wrappers/ISO8601/ISO8601Codable.swift From 9c9d53d85aa8c03d3fc7834457784a1c2f8e2f23 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Thu, 10 Apr 2025 10:13:27 +1000 Subject: [PATCH 21/26] Refactoring property wrappers --- .../Implementations/CodableWrapperMacro.swift | 39 ++++ .../Implementations/EpochWrapperMacro.swift | 39 ++++ .../Implementations/ISO8601WrapperMacro.swift | 39 ++++ .../OptionalDayCodableMacro.swift | 40 ++++ Macros/Implementations/Plugins.swift | 12 + .../Implementations/Syntax+Extensions.swift | 37 +++ Macros/Module/Macros.swift | 15 ++ Package.resolved | 15 ++ Package.swift | 26 +++ .../DateString/DateString.swift | 20 -- .../DateString/DateStringCodable.swift | 53 ----- .../DateString/DateStringConfigurator.swift | 41 ---- Sources/Property wrappers/DayFormatters.swift | 30 +++ .../DayString/DayString.swift | 10 + .../DayString/DayStringCodable.swift | 33 +++ Sources/Property wrappers/Epoch/Epoch.swift | 7 + .../Epoch/EpochCodable.swift | 39 +--- .../Epoch/EpochMilliseconds.swift | 20 -- .../Epoch/EpochSeconds.swift | 20 -- .../ISO8601/CustomISO8601.swift | 20 -- .../ISO8601/CustomISO8601Configurator.swift | 25 -- .../Property wrappers/ISO8601/ISO8601.swift | 21 +- .../ISO8601/ISO8601Codable.swift | 50 ++-- .../CustomISO8601Tests.swift | 12 +- Tests/Property wrappers/DateStringTests.swift | 216 +++++++++--------- Tests/Property wrappers/ISO8601Tests.swift | 202 ++++++++-------- Tests/Property wrappers/MacroTests.swift | 12 + 27 files changed, 594 insertions(+), 499 deletions(-) create mode 100644 Macros/Implementations/CodableWrapperMacro.swift create mode 100644 Macros/Implementations/EpochWrapperMacro.swift create mode 100644 Macros/Implementations/ISO8601WrapperMacro.swift create mode 100644 Macros/Implementations/OptionalDayCodableMacro.swift create mode 100644 Macros/Implementations/Plugins.swift create mode 100644 Macros/Implementations/Syntax+Extensions.swift create mode 100644 Macros/Module/Macros.swift create mode 100644 Package.resolved delete mode 100644 Sources/Property wrappers/DateString/DateString.swift delete mode 100644 Sources/Property wrappers/DateString/DateStringCodable.swift delete mode 100644 Sources/Property wrappers/DateString/DateStringConfigurator.swift create mode 100644 Sources/Property wrappers/DayFormatters.swift create mode 100644 Sources/Property wrappers/DayString/DayString.swift create mode 100644 Sources/Property wrappers/DayString/DayStringCodable.swift create mode 100644 Sources/Property wrappers/Epoch/Epoch.swift delete mode 100644 Sources/Property wrappers/Epoch/EpochMilliseconds.swift delete mode 100644 Sources/Property wrappers/Epoch/EpochSeconds.swift delete mode 100644 Sources/Property wrappers/ISO8601/CustomISO8601.swift delete mode 100644 Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift create mode 100644 Tests/Property wrappers/MacroTests.swift diff --git a/Macros/Implementations/CodableWrapperMacro.swift b/Macros/Implementations/CodableWrapperMacro.swift new file mode 100644 index 0000000..631c372 --- /dev/null +++ b/Macros/Implementations/CodableWrapperMacro.swift @@ -0,0 +1,39 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct CodableWrapperMacro: DeclarationMacro { + + static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + + guard let typeName = node.arguments.stringArgumentValue("typeName"), + let formatter = node.arguments.argumentValue("formatter") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("typeName or formatter argument not supplied. Passed arguments \(node.arguments)") + )) + return [] + } + + return [ + """ + @propertyWrapper + public struct \(raw: typeName): Codable { + + public var wrappedValue: DayType + + public init(wrappedValue: DayType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + wrappedValue = try DayType.decode(using: decoder, formatter: \(raw: formatter)) + } + + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(using: encoder, formatter: \(raw: formatter)) + } + } + """, + ] + } +} diff --git a/Macros/Implementations/EpochWrapperMacro.swift b/Macros/Implementations/EpochWrapperMacro.swift new file mode 100644 index 0000000..091ae6f --- /dev/null +++ b/Macros/Implementations/EpochWrapperMacro.swift @@ -0,0 +1,39 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct EpochWrapperMacro: DeclarationMacro { + + static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + + guard let typeName = node.arguments.stringArgumentValue("typeName"), + let milliseconds = node.arguments.argumentValue("milliseconds") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("typeName or milliseconds argument not supplied. Passed arguments \(node.arguments)") + )) + return [] + } + + return [ + """ + @propertyWrapper + public struct \(raw: typeName): Codable { + + public var wrappedValue: DayType + + public init(wrappedValue: DayType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + wrappedValue = try DayType.decode(using: decoder, milliseconds: \(raw: milliseconds)) + } + + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(using: encoder, milliseconds: \(raw: milliseconds)) + } + } + """, + ] + } +} diff --git a/Macros/Implementations/ISO8601WrapperMacro.swift b/Macros/Implementations/ISO8601WrapperMacro.swift new file mode 100644 index 0000000..bc5b149 --- /dev/null +++ b/Macros/Implementations/ISO8601WrapperMacro.swift @@ -0,0 +1,39 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct ISO8601WrapperMacro: DeclarationMacro { + + static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + + guard let typeName = node.arguments.stringArgumentValue("typeName"), + let formatter = node.arguments.argumentValue("formatter") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("typeName or formatter argument not supplied. Passed arguments \(node.arguments)") + )) + return [] + } + + return [ + """ + @propertyWrapper + public struct \(raw: typeName): Codable { + + public var wrappedValue: DayType + + public init(wrappedValue: DayType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + wrappedValue = try DayType.decode(using: decoder, formatter: \(raw: formatter)) + } + + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(using: encoder, formatter: \(raw: formatter)) + } + } + """, + ] + } +} diff --git a/Macros/Implementations/OptionalDayCodableMacro.swift b/Macros/Implementations/OptionalDayCodableMacro.swift new file mode 100644 index 0000000..91516aa --- /dev/null +++ b/Macros/Implementations/OptionalDayCodableMacro.swift @@ -0,0 +1,40 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct OptionalDayCodableMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf _: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard let argumentName = node.arguments?.stringArgumentValue("argumentName"), + let argumentType = node.arguments?.typeArgumentValue("argumentType") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("argumentName or argumentType not supplied. Passed arguments: \(node.arguments?.description ?? "")") + )) + return [] + } + + return [ + """ + public static func decode(using decoder: Decoder, \(raw: argumentName): \(raw: argumentType)) throws -> Day? { + let container = try decoder.singleValueContainer() + return container.decodeNil() ? nil : try Day.decode(using: decoder, \(raw: argumentName): \(raw: argumentName)) + } + """, + """ + public func encode(using encoder: Encoder, \(raw: argumentName): \(raw: argumentType)) throws { + if let self { + try self.encode(using: encoder, \(raw: argumentName): \(raw: argumentName)) + } else { + var container = encoder.singleValueContainer() + try container.encodeNil() + } + } + """, + ] + } +} diff --git a/Macros/Implementations/Plugins.swift b/Macros/Implementations/Plugins.swift new file mode 100644 index 0000000..e9b10ca --- /dev/null +++ b/Macros/Implementations/Plugins.swift @@ -0,0 +1,12 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct MyMacroPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + CodableWrapperMacro.self, + EpochWrapperMacro.self, + ISO8601WrapperMacro.self, + OptionalDayCodableMacro.self + ] +} diff --git a/Macros/Implementations/Syntax+Extensions.swift b/Macros/Implementations/Syntax+Extensions.swift new file mode 100644 index 0000000..890fe60 --- /dev/null +++ b/Macros/Implementations/Syntax+Extensions.swift @@ -0,0 +1,37 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +extension AttributeSyntax.Arguments { + + func stringArgumentValue(_ key: String) -> String? { + asLabeledExpressionList?.stringArgumentValue(key) + } + + func typeArgumentValue(_ key: String) -> String? { + asLabeledExpressionList?.typeArgumentValue(key) + } + + private var asLabeledExpressionList: LabeledExprListSyntax? { + self.as(LabeledExprListSyntax.self) + } +} + +extension LabeledExprListSyntax { + + func argumentValue(_ key: String) -> String? { + expression(key)?.trimmedDescription + } + + func stringArgumentValue(_ key: String) -> String? { + expression(key)?.as(StringLiteralExprSyntax.self)?.segments.trimmedDescription + } + + func typeArgumentValue(_ key: String) -> String? { + expression(key)?.as(MemberAccessExprSyntax.self)?.base?.trimmedDescription + } + + private func expression(_ key: String) -> ExprSyntax? { + first(where: { $0.label?.text == key })?.expression + } +} diff --git a/Macros/Module/Macros.swift b/Macros/Module/Macros.swift new file mode 100644 index 0000000..001fa48 --- /dev/null +++ b/Macros/Module/Macros.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftSyntax + +@freestanding(declaration, names: arbitrary) +public macro dayStringCodablePropertyWrapper(typeName: String, formatter: DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "CodableWrapperMacro") + +@freestanding(declaration, names: arbitrary) +public macro epochCodablePropertyWrapper(typeName: String, milliseconds: Bool) = #externalMacro(module: "DayTypeMacroImplementations", type: "EpochWrapperMacro") + +@freestanding(declaration, names: arbitrary) +public macro ios8601CodablePropertyWrapper(typeName: String, formatter: ISO8601DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "ISO8601WrapperMacro") + +@attached(member, names: named(decode), named(encode)) +public macro OptionalDayCodable(argumentName: String, argumentType: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "OptionalDayCodableMacro") + diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..a3b73cc --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "99bab288a62c38d106a0d481d77a387440627f8599eb73490986122e3076ff0c", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index bbc3633..9203cc6 100755 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,6 @@ // swift-tools-version:5.10 +import CompilerPluginSupport import PackageDescription let package = Package( @@ -10,18 +11,43 @@ let package = Package( ], products: [ .library(name: "DayType", targets: ["DayType"]), + .library(name: "DayTypeMacros", targets: ["DayTypeMacros"]), ], dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), ], targets: [ .target( name: "DayType", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + "DayTypeMacros", + ], path: "Sources" ), + .target( + name: "DayTypeMacros", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + "DayTypeMacroImplementations", + ], + path: "Macros/Module" + ), + .macro( + name: "DayTypeMacroImplementations", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftDiagnostics", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ], + path: "Macros/Implementations" + ), .testTarget( name: "DayTypeTests", dependencies: [ "DayType", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), ], path: "Tests" ), diff --git a/Sources/Property wrappers/DateString/DateString.swift b/Sources/Property wrappers/DateString/DateString.swift deleted file mode 100644 index 01521d6..0000000 --- a/Sources/Property wrappers/DateString/DateString.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// Identifies a ``Day`` property that reads and writes from date strings using a configured ``DateFormatter``. -@propertyWrapper -public struct DateString: Codable where T: DateStringCodable, Configurator: DateStringConfigurator { - - public var wrappedValue: T - - public init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try T(dateDecoder: decoder, configurator: Configurator.self) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(dateEncoder: encoder, configurator: Configurator.self) - } -} diff --git a/Sources/Property wrappers/DateString/DateStringCodable.swift b/Sources/Property wrappers/DateString/DateStringCodable.swift deleted file mode 100644 index 0d9c4e3..0000000 --- a/Sources/Property wrappers/DateString/DateStringCodable.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -/// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. -/// -/// By using this protocols for property wrappers we can reduce the number of wrappers needed because -/// it erases the optional aspect of the values. -public protocol DateStringCodable { - init(dateDecoder decoder: Decoder, configurator: (some DateStringConfigurator).Type) throws - func encode(dateEncoder encoder: Encoder, configurator: (some DateStringConfigurator).Type) throws -} - -extension Day: DateStringCodable { - - public init(dateDecoder decoder: Decoder, configurator: (some DateStringConfigurator).Type) throws { - - let container = try decoder.singleValueContainer() - let formatter = DateFormatter() - configurator.configure(formatter: formatter) - - guard let dateString = try? container.decode(String.self), - let date = formatter.date(from: dateString) else { - let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read the date string.") - throw DecodingError.dataCorrupted(context) - } - - self = Day(date: date) - } - - public func encode(dateEncoder encoder: Encoder, configurator: (some DateStringConfigurator).Type) throws { - var container = encoder.singleValueContainer() - let formatter = DateFormatter() - configurator.configure(formatter: formatter) - try container.encode(formatter.string(from: date())) - } -} - -/// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -extension Day?: DateStringCodable { - - public init(dateDecoder decoder: Decoder, configurator: (some DateStringConfigurator).Type) throws { - let container = try decoder.singleValueContainer() - self = container.decodeNil() ? nil : try Day(dateDecoder: decoder, configurator: configurator) - } - - public func encode(dateEncoder encoder: Encoder, configurator: (some DateStringConfigurator).Type) throws { - if let self { - try self.encode(dateEncoder: encoder, configurator: configurator) - } else { - var container = encoder.singleValueContainer() - try container.encodeNil() - } - } -} diff --git a/Sources/Property wrappers/DateString/DateStringConfigurator.swift b/Sources/Property wrappers/DateString/DateStringConfigurator.swift deleted file mode 100644 index 086d58b..0000000 --- a/Sources/Property wrappers/DateString/DateStringConfigurator.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -/// Sets up the ``DateFormatter`` used for decoding and encoding date strings. -public protocol DateStringConfigurator { - - /// Called when setting up to decode or encode a date string. - static func configure(formatter: DateFormatter) -} - -public extension DateStringConfigurator { - - static var ISO: DateStringConfigurator.Type { DateFormatterConfig.ISO.self } -} - -/// Useful common configurations of date formatters. -public enum DateFormatterConfig { - - public enum ISO: DateStringConfigurator { - public static func configure(formatter: DateFormatter) { - formatter.dateFormat = "yyyy-MM-dd" - } - } - - public enum DMY: DateStringConfigurator { - public static func configure(formatter: DateFormatter) { - formatter.dateFormat = "dd/MM/yyyy" - } - } - - public enum MDY: DateStringConfigurator { - public static func configure(formatter: DateFormatter) { - formatter.dateFormat = "MM/dd/yyyy" - } - } -} - -struct ISO: DateStringConfigurator { - public static func configure(formatter: DateFormatter) { - formatter.dateFormat = "yyyy-MM-dd" - } -} diff --git a/Sources/Property wrappers/DayFormatters.swift b/Sources/Property wrappers/DayFormatters.swift new file mode 100644 index 0000000..260918f --- /dev/null +++ b/Sources/Property wrappers/DayFormatters.swift @@ -0,0 +1,30 @@ +import Foundation + +enum DayFormatters { + + public static let dmy = { + let formatter = DateFormatter() + formatter.dateFormat = "dd/MM/yyyy" + return formatter + }() + + public static let mdy = { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd/yyyy" + return formatter + }() + + public static let ymd = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + + public static let iso8601 = ISO8601DateFormatter() + + public static let iso8601SansTimezone = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions.remove(.withTimeZone) + return formatter + }() +} diff --git a/Sources/Property wrappers/DayString/DayString.swift b/Sources/Property wrappers/DayString/DayString.swift new file mode 100644 index 0000000..efc3aca --- /dev/null +++ b/Sources/Property wrappers/DayString/DayString.swift @@ -0,0 +1,10 @@ +import Foundation +import DayTypeMacros + +/// Identifies a ``Day`` property that reads and writes from day strings using a formatter. +public enum DayString where DayType: DayStringCodable { + #dayStringCodablePropertyWrapper(typeName: "DMY", formatter: DayFormatters.dmy) + #dayStringCodablePropertyWrapper(typeName: "MDY", formatter: DayFormatters.mdy) + #dayStringCodablePropertyWrapper(typeName: "YMD", formatter: DayFormatters.ymd) +} + diff --git a/Sources/Property wrappers/DayString/DayStringCodable.swift b/Sources/Property wrappers/DayString/DayStringCodable.swift new file mode 100644 index 0000000..378371d --- /dev/null +++ b/Sources/Property wrappers/DayString/DayStringCodable.swift @@ -0,0 +1,33 @@ +import Foundation +import DayTypeMacros + +/// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. +/// +/// By using this protocols for property wrappers we can reduce the number of wrappers needed because +/// it erases the optional aspect of the values. +public protocol DayStringCodable { + static func decode(using decoder: Decoder, formatter: DateFormatter) throws -> Self + func encode(using encoder: Encoder, formatter: DateFormatter) throws +} + +extension Day: DayStringCodable { + + public static func decode(using decoder: Decoder, formatter: DateFormatter) throws -> Day { + let container = try decoder.singleValueContainer() + guard let dateString = try? container.decode(String.self), + let date = formatter.date(from: dateString) else { + let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read the date string.") + throw DecodingError.dataCorrupted(context) + } + return Day(date: date) + } + + public func encode(using encoder: Encoder, formatter: DateFormatter) throws { + var container = encoder.singleValueContainer() + try container.encode(formatter.string(from: date())) + } +} + +///// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. +@OptionalDayCodable(argumentName: "formatter", argumentType: DateFormatter.self) +extension Day?: DayStringCodable {} diff --git a/Sources/Property wrappers/Epoch/Epoch.swift b/Sources/Property wrappers/Epoch/Epoch.swift new file mode 100644 index 0000000..f8f2700 --- /dev/null +++ b/Sources/Property wrappers/Epoch/Epoch.swift @@ -0,0 +1,7 @@ +import Foundation +import DayTypeMacros + +public enum Epoch where DayType: EpochCodable { + #epochCodablePropertyWrapper(typeName: "Milliseconds", milliseconds: true) + #epochCodablePropertyWrapper(typeName: "Seconds", milliseconds: false) +} diff --git a/Sources/Property wrappers/Epoch/EpochCodable.swift b/Sources/Property wrappers/Epoch/EpochCodable.swift index 8e15c1e..2e41d66 100644 --- a/Sources/Property wrappers/Epoch/EpochCodable.swift +++ b/Sources/Property wrappers/Epoch/EpochCodable.swift @@ -1,55 +1,32 @@ import Foundation +import DayTypeMacros /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. /// /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol EpochCodable { - - /// Decoding with optional milliseconds. - init(epochDecoder decoder: Decoder, withMilliseconds: Bool) throws - - /// Encoding with optional milliseconds. - func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws -} - -/// Providing defaults. -public extension EpochCodable { - init(epochDecoder decoder: Decoder) throws { try self.init(epochDecoder: decoder, withMilliseconds: false) } - func encode(epochEncoder encoder: Encoder) throws { try encode(epochEncoder: encoder, withMilliseconds: false) } + static func decode(using decoder: Decoder, milliseconds: Bool) throws -> Self + func encode(using encoder: Encoder, milliseconds: Bool) throws } extension Day: EpochCodable { - public init(epochDecoder decoder: Decoder, withMilliseconds milliseconds: Bool) throws { + public static func decode(using decoder: Decoder, milliseconds: Bool) throws -> Day { let container = try decoder.singleValueContainer() guard let epochTime = try? container.decode(TimeInterval.self) else { let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read a Day value, expected an epoch.") throw DecodingError.dataCorrupted(context) } - self = Day(date: Date(timeIntervalSince1970: epochTime / (milliseconds ? 1000 : 1))) + return Day(date: Date(timeIntervalSince1970: epochTime / (milliseconds ? 1000 : 1))) } - public func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws { + public func encode(using encoder: Encoder, milliseconds: Bool) throws { var container = encoder.singleValueContainer() try container.encode(date().timeIntervalSince1970 * (milliseconds ? 1000 : 1)) } } /// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -extension Day?: EpochCodable { - - public init(epochDecoder decoder: Decoder, withMilliseconds milliseconds: Bool) throws { - let container = try decoder.singleValueContainer() - self = container.decodeNil() ? nil : try Day(epochDecoder: decoder, withMilliseconds: milliseconds) - } - - public func encode(epochEncoder encoder: Encoder, withMilliseconds milliseconds: Bool) throws { - if let self { - try self.encode(epochEncoder: encoder, withMilliseconds: milliseconds) - } else { - var container = encoder.singleValueContainer() - try container.encodeNil() - } - } -} +@OptionalDayCodable(argumentName: "milliseconds", argumentType: Bool.self) +extension Day?: EpochCodable {} diff --git a/Sources/Property wrappers/Epoch/EpochMilliseconds.swift b/Sources/Property wrappers/Epoch/EpochMilliseconds.swift deleted file mode 100644 index 495da37..0000000 --- a/Sources/Property wrappers/Epoch/EpochMilliseconds.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// Identifies a ``Day`` property that reads and writes from an epoch time value expressed in seconds. -@propertyWrapper -public struct EpochMilliseconds: Codable where T: EpochCodable { - - public var wrappedValue: T - - public init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try T(epochDecoder: decoder, withMilliseconds: true) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(epochEncoder: encoder, withMilliseconds: true) - } -} diff --git a/Sources/Property wrappers/Epoch/EpochSeconds.swift b/Sources/Property wrappers/Epoch/EpochSeconds.swift deleted file mode 100644 index 5029ffc..0000000 --- a/Sources/Property wrappers/Epoch/EpochSeconds.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// Identifies a ``Day`` property that reads and writes from an epoch time value expressed in seconds. -@propertyWrapper -public struct EpochSeconds: Codable where T: EpochCodable { - - public var wrappedValue: T - - public init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try T(epochDecoder: decoder) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(epochEncoder: encoder) - } -} diff --git a/Sources/Property wrappers/ISO8601/CustomISO8601.swift b/Sources/Property wrappers/ISO8601/CustomISO8601.swift deleted file mode 100644 index ab10ac6..0000000 --- a/Sources/Property wrappers/ISO8601/CustomISO8601.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// Identifies a ``Day`` property that reads and writes from an ISO8601 formatted string. -@propertyWrapper -public struct CustomISO8601: Codable where T: ISO8601Codable, Configurator: CustomISO8601Configurator { - - public var wrappedValue: T - - public init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try T(iso8601Decoder: decoder, configurator: Configurator.self) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(iso8601Encoder: encoder, configurator: Configurator.self) - } -} diff --git a/Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift b/Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift deleted file mode 100644 index c5854fb..0000000 --- a/Sources/Property wrappers/ISO8601/CustomISO8601Configurator.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -/// Sets up the ``ISO8601DateFormatter`` used for decoding and encoding date strings. -public protocol CustomISO8601Configurator { - - /// Called when setting up to decode or encode an ISO8601 date string. - static func configure(formatter: ISO8601DateFormatter) -} - -/// Useful common configurations of ISO8601 formatters. -public enum ISO8601Config { - - /// A default implementation that leaves the formatted untouched from it's defaults. - /// in the default property wrappers. - public enum Default: CustomISO8601Configurator { - public static func configure(formatter _: ISO8601DateFormatter) {} - } - - /// Tells the formatter to ignores timezone values. - public enum SansTimeZone: CustomISO8601Configurator { - public static func configure(formatter: ISO8601DateFormatter) { - formatter.formatOptions.remove(.withTimeZone) - } - } -} diff --git a/Sources/Property wrappers/ISO8601/ISO8601.swift b/Sources/Property wrappers/ISO8601/ISO8601.swift index f2f5ab8..dd1b345 100644 --- a/Sources/Property wrappers/ISO8601/ISO8601.swift +++ b/Sources/Property wrappers/ISO8601/ISO8601.swift @@ -1,20 +1,7 @@ import Foundation +import DayTypeMacros -/// Identifies a ``Day`` property that reads and writes from an ISO8601 formatted string. -@propertyWrapper -public struct ISO8601: Codable where T: ISO8601Codable { - - public var wrappedValue: T - - public init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try T(iso8601Decoder: decoder, configurator: ISO8601Config.Default.self) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(iso8601Encoder: encoder, configurator: ISO8601Config.Default.self) - } +public enum ISO8601 where DayType: ISO8601Codable { + #ios8601CodablePropertyWrapper(typeName: "Default", formatter: DayFormatters.iso8601) + #ios8601CodablePropertyWrapper(typeName: "SansTimezone", formatter: DayFormatters.iso8601SansTimezone) } diff --git a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift index 4cada2d..101b4a7 100644 --- a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift +++ b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift @@ -1,3 +1,4 @@ +import DayTypeMacros import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. @@ -5,53 +6,28 @@ import Foundation /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol ISO8601Codable { - init(iso8601Decoder decoder: Decoder, configurator: (some CustomISO8601Configurator).Type) throws - func encode(iso8601Encoder encoder: Encoder, configurator: (some CustomISO8601Configurator).Type) throws + static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Self + func encode(using encoder: Encoder, formatter: ISO8601DateFormatter) throws } -/// Adds ``DayCodable`` to ``Day``. extension Day: ISO8601Codable { - public init(iso8601Decoder decoder: Decoder, configurator: (some CustomISO8601Configurator).Type) throws { - + public static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day { let container = try decoder.singleValueContainer() - if let iso8601String = try? container.decode(String.self) { - let reader = ISO8601DateFormatter() - configurator.configure(formatter: reader) - if let date = reader.date(from: iso8601String) { - self.init(date: date) - return - } + guard let iso8601String = try? container.decode(String.self), + let date = formatter.date(from: iso8601String) else { + let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read a Day value, expected a valid ISO8601 string.") + throw DecodingError.dataCorrupted(context) } - - let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read a Day value, expected a valid ISO8601 string.") - throw DecodingError.dataCorrupted(context) + return Day(date: date) } - public func encode(iso8601Encoder encoder: Encoder, configurator: (some CustomISO8601Configurator).Type) throws { - let writer = ISO8601DateFormatter() - configurator.configure(formatter: writer) - let iso8601String = writer.string(from: date()) + public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter) throws { var container = encoder.singleValueContainer() - try container.encode(iso8601String) + try container.encode(formatter.string(from: date())) } } /// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -extension Day?: ISO8601Codable { - - public init(iso8601Decoder decoder: Decoder, configurator: (some CustomISO8601Configurator).Type) throws { - let container = try decoder.singleValueContainer() - self = container.decodeNil() ? nil : try Day(iso8601Decoder: decoder, configurator: configurator) - } - - public func encode(iso8601Encoder encoder: Encoder, configurator: (some CustomISO8601Configurator).Type) throws { - if let self { - try self.encode(iso8601Encoder: encoder, configurator: configurator) - } else { - var container = encoder.singleValueContainer() - try container.encodeNil() - } - } -} - +@OptionalDayCodable(argumentName: "formatter", argumentType: ISO8601DateFormatter.self) +extension Day?: ISO8601Codable {} diff --git a/Tests/Property wrappers/CustomISO8601Tests.swift b/Tests/Property wrappers/CustomISO8601Tests.swift index 0a585b9..ce0cd1d 100644 --- a/Tests/Property wrappers/CustomISO8601Tests.swift +++ b/Tests/Property wrappers/CustomISO8601Tests.swift @@ -24,7 +24,7 @@ extension PropertyWrapperSuites { @Test("Sans Timezone decoding") func sansTimeZoneDecoding() throws { let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 03)) } @@ -70,34 +70,34 @@ extension PropertyWrapperSuites { @Test("Optional sans timezone decoding") func optionalSansTimeZoneDecoding() throws { let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == Day(2012, 02, 03)) } @Test("Sans timezone decoding of nil") func optionalSansTimeZoneWithNilDecoding() throws { let json = #"{"d1":null}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) #expect(result.d1 == nil) } @Test("Custom ISO8601 encoding") func encodingISO8601() throws { - let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) + let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) let result = try JSONEncoder().encode(instance) #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) } @Test("Optional Custom ISO8601 encoding") func encodingToOptional() throws { - let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) + let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) let result = try JSONEncoder().encode(instance) #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) } @Test("Optional Custom ISO8601 encoding of nil") func testEncodingNil() throws { - let instance = ISO8601CustomOptionalContainer(d1: nil) + let instance = ISO8601CustomOptionalContainer(d1: nil) let result = try JSONEncoder().encode(instance) #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) } diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 93e9921..4db08b9 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -1,108 +1,108 @@ -import DayType -import Foundation -import Testing - -private struct DayContainer: Codable where Configurator: DateStringConfigurator { - @DateString var d1: Day -} - -private struct OptionalDayContainer: Codable where Configurator: DateStringConfigurator { - @DateString var d1: Day? -} - -extension PropertyWrapperSuites { - - @Suite("@DateString") - struct DateStringTests { - - @Test("ISO8601 date decoding") - func decodingAnISO8601Date() throws { - let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("DMY date decoding") - func decodingADMYDate() throws { - let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("MDY date decoding") - func decodingAMDYDate() throws { - let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("Optional ISO8601 date decoding") - func decodingAnIOptionalISO8601Date() throws { - let json = #"{"d1": "2012-02-01"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("DMY date") - func decodingAnOptionalDMYDate() throws { - let json = #"{"d1": "01/02/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("MDY date decoding") - func decodingAnOptionalMDYDate() throws { - let json = #"{"d1": "02/01/2012"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 01)) - } - - @Test("Nil date decoding") - func decodingANilDate() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } - - @Test("Invalid date decoding throws an error") - func decodingInvalidDateThrows() throws { - do { - let json = #"{"d1": "xxx"}"# - _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - #expect(context.codingPath.map(\.stringValue) == ["d1"]) - #expect(context.debugDescription == "Unable to read the date string.") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("DMY date encoding") - func encodingDateString() throws { - let instance = DayContainer(d1: Day(2012, 02, 01)) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) - } - - @Test("Optional DMY date encoding") - func encodingOptionalDateString() throws { - let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) - } - - @Test("Optional DMY date encoding from a nil") - func encodingOptionalDateStringWithNil() throws { - let instance = OptionalDayContainer(d1: nil) - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let result = try encoder.encode(instance) - #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) - } - } -} +//import DayType +//import Foundation +//import Testing +// +//private struct DayContainer: Codable where Configuration: DayStringConfiguration { +// @DayString var d1: Day +//} +// +//private struct OptionalDayContainer: Codable where Configuration: DayStringConfiguration { +// @DayString var d1: Day? +//} +// +//extension PropertyWrapperSuites { +// +// @Suite("@DayString") +// struct DateStringTests { +// +// @Test("ISO8601 date decoding") +// func decodingAnISO8601Date() throws { +// let json = #"{"d1": "2012-02-01"}"# +// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("DMY date decoding") +// func decodingADMYDate() throws { +// let json = #"{"d1": "01/02/2012"}"# +// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("MDY date decoding") +// func decodingAMDYDate() throws { +// let json = #"{"d1": "02/01/2012"}"# +// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("Optional ISO8601 date decoding") +// func decodingAnIOptionalISO8601Date() throws { +// let json = #"{"d1": "2012-02-01"}"# +// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("DMY date") +// func decodingAnOptionalDMYDate() throws { +// let json = #"{"d1": "01/02/2012"}"# +// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("MDY date decoding") +// func decodingAnOptionalMDYDate() throws { +// let json = #"{"d1": "02/01/2012"}"# +// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 01)) +// } +// +// @Test("Nil date decoding") +// func decodingANilDate() throws { +// let json = #"{"d1": null}"# +// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == nil) +// } +// +// @Test("Invalid date decoding throws an error") +// func decodingInvalidDateThrows() throws { +// do { +// let json = #"{"d1": "xxx"}"# +// _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.dataCorrupted(let context) { +// #expect(context.codingPath.map(\.stringValue) == ["d1"]) +// #expect(context.debugDescription == "Unable to read the date string.") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("DMY date encoding") +// func encodingDateString() throws { +// let instance = DayContainer(d1: Day(2012, 02, 01)) +// let encoder = JSONEncoder() +// encoder.outputFormatting = .withoutEscapingSlashes +// let result = try encoder.encode(instance) +// #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) +// } +// +// @Test("Optional DMY date encoding") +// func encodingOptionalDateString() throws { +// let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) +// let encoder = JSONEncoder() +// encoder.outputFormatting = .withoutEscapingSlashes +// let result = try encoder.encode(instance) +// #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) +// } +// +// @Test("Optional DMY date encoding from a nil") +// func encodingOptionalDateStringWithNil() throws { +// let instance = OptionalDayContainer(d1: nil) +// let encoder = JSONEncoder() +// encoder.outputFormatting = .withoutEscapingSlashes +// let result = try encoder.encode(instance) +// #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) +// } +// } +//} diff --git a/Tests/Property wrappers/ISO8601Tests.swift b/Tests/Property wrappers/ISO8601Tests.swift index 2a9e503..827571d 100644 --- a/Tests/Property wrappers/ISO8601Tests.swift +++ b/Tests/Property wrappers/ISO8601Tests.swift @@ -1,101 +1,101 @@ -import DayType -import Foundation -import Testing - -private struct ISO8601Container: Codable { - - @ISO8601 var d1: Day - init(d1: Day) { - self.d1 = d1 - } -} - -private struct ISO8601OptionalContainer: Codable { - @ISO8601 var d1: Day? - init(d1: Day?) { - self.d1 = d1 - } -} - -extension PropertyWrapperSuites { - - @Suite("@ISO8601") - struct ISO8601Tests { - - @Test("Decoding") - func decoding() throws { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding a GMT value") - func decodingWithDefaultGMT() throws { - let json = #"{"d1": "2012-02-02T13:33:23Z"}"# - let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding invalid value") - func decodingWithInvalidStringDate() throws { - do { - let json = #"{"d1": "xxxx"}"# - _ = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - #expect(context.debugDescription == "Unable to read a Day value, expected a valid ISO8601 string.") - #expect(context.codingPath.last?.stringValue == "d1") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Decoding optional") - func decodingOptional() throws { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding optional with nil") - func decodingOptionalWithNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } - - @Test("Decoding optional with missing value") - func decodingOptionalWithMissingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - #expect(key.stringValue == "d1") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Encoding") - func encoding() throws { - let instance = ISO8601Container(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) - } - - @Test("Encoding optional") - func encodingOptional() throws { - let instance = ISO8601OptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) - } - - @Test("Encoding optional with nil") - func encodingOptionalNil() throws { - let instance = ISO8601OptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) - } - } -} +//import DayType +//import Foundation +//import Testing +// +//private struct ISO8601Container: Codable { +// +// @ISO8601 var d1: Day +// init(d1: Day) { +// self.d1 = d1 +// } +//} +// +//private struct ISO8601OptionalContainer: Codable { +// @ISO8601 var d1: Day? +// init(d1: Day?) { +// self.d1 = d1 +// } +//} +// +//extension PropertyWrapperSuites { +// +// @Suite("@ISO8601") +// struct ISO8601Tests { +// +// @Test("Decoding") +// func decoding() throws { +// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# +// let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding a GMT value") +// func decodingWithDefaultGMT() throws { +// let json = #"{"d1": "2012-02-02T13:33:23Z"}"# +// let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding invalid value") +// func decodingWithInvalidStringDate() throws { +// do { +// let json = #"{"d1": "xxxx"}"# +// _ = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.dataCorrupted(let context) { +// #expect(context.debugDescription == "Unable to read a Day value, expected a valid ISO8601 string.") +// #expect(context.codingPath.last?.stringValue == "d1") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Decoding optional") +// func decodingOptional() throws { +// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# +// let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding optional with nil") +// func decodingOptionalWithNil() throws { +// let json = #"{"d1": null}"# +// let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == nil) +// } +// +// @Test("Decoding optional with missing value") +// func decodingOptionalWithMissingValue() throws { +// do { +// let json = #"{}"# +// _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.keyNotFound(let key, _) { +// #expect(key.stringValue == "d1") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Encoding") +// func encoding() throws { +// let instance = ISO8601Container(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) +// } +// +// @Test("Encoding optional") +// func encodingOptional() throws { +// let instance = ISO8601OptionalContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) +// } +// +// @Test("Encoding optional with nil") +// func encodingOptionalNil() throws { +// let instance = ISO8601OptionalContainer(d1: nil) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) +// } +// } +//} diff --git a/Tests/Property wrappers/MacroTests.swift b/Tests/Property wrappers/MacroTests.swift new file mode 100644 index 0000000..02519d6 --- /dev/null +++ b/Tests/Property wrappers/MacroTests.swift @@ -0,0 +1,12 @@ +import Testing +import DayType + +struct X { + + @DayString.DMY + var a: Day + + @DayString.DMY + var b: Day? + +} From bfb3f75ec1ff20dd8f77c342a42c867db625b2e3 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 11 May 2025 16:26:18 +1000 Subject: [PATCH 22/26] New property wrapper for DayString --- .../Implementations/CodableWrapperMacro.swift | 39 --- ...DayStringBuiltinPropertyWrapperMacro.swift | 59 +++++ .../Implementations/EpochWrapperMacro.swift | 2 +- .../Implementations/ISO8601WrapperMacro.swift | 4 +- .../KeyedContainerDecodeMissingMacro.swift | 35 +++ .../KeyedContainerEncodeMissingMacro.swift | 41 +++ .../OptionalDayCodableMacro.swift | 40 --- Macros/Implementations/Plugins.swift | 5 +- .../Implementations/Syntax+Extensions.swift | 4 + Macros/Module/Macros.swift | 21 +- Package.swift | 2 - .../DayString/DayString.swift | 21 +- .../DayString/DayStringCodable.swift | 31 ++- .../Epoch/EpochCodable.swift | 24 +- .../ISO8601/ISO8601Codable.swift | 21 +- .../CustomISO8601Tests.swift | 210 ++++++++-------- Tests/Property wrappers/DateStringTests.swift | 236 ++++++++++-------- .../EpochMillisecondsTests.swift | 186 +++++++------- .../Property wrappers/EpochSecondsTests.swift | 186 +++++++------- Tests/Property wrappers/MacroTests.swift | 12 - 20 files changed, 653 insertions(+), 526 deletions(-) delete mode 100644 Macros/Implementations/CodableWrapperMacro.swift create mode 100644 Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift create mode 100644 Macros/Implementations/KeyedContainerDecodeMissingMacro.swift create mode 100644 Macros/Implementations/KeyedContainerEncodeMissingMacro.swift delete mode 100644 Macros/Implementations/OptionalDayCodableMacro.swift delete mode 100644 Tests/Property wrappers/MacroTests.swift diff --git a/Macros/Implementations/CodableWrapperMacro.swift b/Macros/Implementations/CodableWrapperMacro.swift deleted file mode 100644 index 631c372..0000000 --- a/Macros/Implementations/CodableWrapperMacro.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxMacros - -struct CodableWrapperMacro: DeclarationMacro { - - static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { - - guard let typeName = node.arguments.stringArgumentValue("typeName"), - let formatter = node.arguments.argumentValue("formatter") else { - context.diagnose(Diagnostic( - node: node, message: MacroExpansionErrorMessage("typeName or formatter argument not supplied. Passed arguments \(node.arguments)") - )) - return [] - } - - return [ - """ - @propertyWrapper - public struct \(raw: typeName): Codable { - - public var wrappedValue: DayType - - public init(wrappedValue: DayType) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try DayType.decode(using: decoder, formatter: \(raw: formatter)) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(using: encoder, formatter: \(raw: formatter)) - } - } - """, - ] - } -} diff --git a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift new file mode 100644 index 0000000..896b4f1 --- /dev/null +++ b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift @@ -0,0 +1,59 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// Adds a static embedeed type for encoding and decoding using a passed formatter. +struct DayStringBuiltinPropertyWrapperMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf _: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard let typeName = node.arguments?.stringArgumentValue("name"), + let formatter = node.arguments?.argumentValue("formatter") + else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("typeName, formatter or writeNulls argument not supplied. Passed arguments: \(node.arguments?.description ?? "")") + )) + return [] + } + + let withNullableImplementation = (node.arguments?.argumentValue("withNullableImplementation") ?? "true") == "true" + + return [ + DeclSyntax(stringLiteral: propertyWrapper(name: typeName, formatter: formatter, withNullableImplementation: withNullableImplementation)), + ] + } + + private static func propertyWrapper(name: String, formatter: String, withNullableImplementation: Bool) -> String { + let writeNulls = withNullableImplementation ? "false" : "true" + return """ + @propertyWrapper + public struct \(name): Codable { + + \(withNullableImplementation ? propertyWrapper(name: "WritesNulls", formatter: formatter, withNullableImplementation: false) : "") + + public var wrappedValue: DayType + // We need to expose these values so keyed containers can access them. + let formatter = \(formatter) + let writeNulls = \(writeNulls) + + public init(wrappedValue: DayType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + wrappedValue = try DayType.decode(from: try decoder.singleValueContainer(), formatter: \(formatter)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try wrappedValue.encode(into: &container, formatter: \(formatter), writeNulls: \(writeNulls)) + } + } + """ + } +} diff --git a/Macros/Implementations/EpochWrapperMacro.swift b/Macros/Implementations/EpochWrapperMacro.swift index 091ae6f..fe8d9ce 100644 --- a/Macros/Implementations/EpochWrapperMacro.swift +++ b/Macros/Implementations/EpochWrapperMacro.swift @@ -30,7 +30,7 @@ struct EpochWrapperMacro: DeclarationMacro { } public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(using: encoder, milliseconds: \(raw: milliseconds)) + try wrappedValue.encode(using: encoder, milliseconds: \(raw: milliseconds), writeNulls: false) } } """, diff --git a/Macros/Implementations/ISO8601WrapperMacro.swift b/Macros/Implementations/ISO8601WrapperMacro.swift index bc5b149..c95fb51 100644 --- a/Macros/Implementations/ISO8601WrapperMacro.swift +++ b/Macros/Implementations/ISO8601WrapperMacro.swift @@ -20,6 +20,8 @@ struct ISO8601WrapperMacro: DeclarationMacro { public struct \(raw: typeName): Codable { public var wrappedValue: DayType + // We need the formatter so that optional property wrappers can write day strings. + var formatter = \(raw: formatter) public init(wrappedValue: DayType) { self.wrappedValue = wrappedValue @@ -30,7 +32,7 @@ struct ISO8601WrapperMacro: DeclarationMacro { } public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(using: encoder, formatter: \(raw: formatter)) + try wrappedValue.encode(using: encoder, formatter: \(raw: formatter), writeNulls: false) } } """, diff --git a/Macros/Implementations/KeyedContainerDecodeMissingMacro.swift b/Macros/Implementations/KeyedContainerDecodeMissingMacro.swift new file mode 100644 index 0000000..2331c8a --- /dev/null +++ b/Macros/Implementations/KeyedContainerDecodeMissingMacro.swift @@ -0,0 +1,35 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct KeyedContainerDecodeMissingMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf owningType: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription == "KeyedDecodingContainer" else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Can only be used on a KeyedDecodingContainer: \(owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription ?? "")") + )) + return [] + } + + guard let decodableType = node.arguments?.typeArgumentValue("type") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Unable to determine passed type argument from: \(node.arguments?.description ?? "")") + )) + return [] + } + + return [ + """ + public func decode(_ type: \(raw: decodableType).Type, forKey key: Key) throws -> \(raw: decodableType) { + try decodeIfPresent(type, forKey: key) ?? .init(wrappedValue: nil) + } + """, + ] + } +} diff --git a/Macros/Implementations/KeyedContainerEncodeMissingMacro.swift b/Macros/Implementations/KeyedContainerEncodeMissingMacro.swift new file mode 100644 index 0000000..3d0d37a --- /dev/null +++ b/Macros/Implementations/KeyedContainerEncodeMissingMacro.swift @@ -0,0 +1,41 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct KeyedContainerEncodeMissingMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf owningType: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription == "KeyedEncodingContainer" else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Can only be used on a KeyedEncodingContainer: \(owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription ?? "")") + )) + return [] + } + + guard let encodableType = node.arguments?.typeArgumentValue("type") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Unable to determine passed type argument from: \(node.arguments?.description ?? "")") + )) + return [] + } + + return [ + """ + /// Encioding must be handled by the keyed container or we end up writing keys with missing values. + public mutating func encode(_ propertyWrapper: \(raw: encodableType), forKey key: Key) throws { + // If there is a value then use the property wrappers to convert it to a string and write that. + if let value = propertyWrapper.wrappedValue { + try self.encode(propertyWrapper.formatter.string(from: value.date()), forKey: key) + } else if propertyWrapper.writeNulls { + try self.encodeNil(forKey: key) + } + } + """, + ] + } +} diff --git a/Macros/Implementations/OptionalDayCodableMacro.swift b/Macros/Implementations/OptionalDayCodableMacro.swift deleted file mode 100644 index 91516aa..0000000 --- a/Macros/Implementations/OptionalDayCodableMacro.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxMacros - -struct OptionalDayCodableMacro: MemberMacro { - - static func expansion( - of node: AttributeSyntax, - providingMembersOf _: some DeclGroupSyntax, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - - guard let argumentName = node.arguments?.stringArgumentValue("argumentName"), - let argumentType = node.arguments?.typeArgumentValue("argumentType") else { - context.diagnose(Diagnostic( - node: node, message: MacroExpansionErrorMessage("argumentName or argumentType not supplied. Passed arguments: \(node.arguments?.description ?? "")") - )) - return [] - } - - return [ - """ - public static func decode(using decoder: Decoder, \(raw: argumentName): \(raw: argumentType)) throws -> Day? { - let container = try decoder.singleValueContainer() - return container.decodeNil() ? nil : try Day.decode(using: decoder, \(raw: argumentName): \(raw: argumentName)) - } - """, - """ - public func encode(using encoder: Encoder, \(raw: argumentName): \(raw: argumentType)) throws { - if let self { - try self.encode(using: encoder, \(raw: argumentName): \(raw: argumentName)) - } else { - var container = encoder.singleValueContainer() - try container.encodeNil() - } - } - """, - ] - } -} diff --git a/Macros/Implementations/Plugins.swift b/Macros/Implementations/Plugins.swift index e9b10ca..1705409 100644 --- a/Macros/Implementations/Plugins.swift +++ b/Macros/Implementations/Plugins.swift @@ -4,9 +4,10 @@ import SwiftSyntaxMacros @main struct MyMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - CodableWrapperMacro.self, + DayStringBuiltinPropertyWrapperMacro.self, EpochWrapperMacro.self, ISO8601WrapperMacro.self, - OptionalDayCodableMacro.self + KeyedContainerDecodeMissingMacro.self, + KeyedContainerEncodeMissingMacro.self ] } diff --git a/Macros/Implementations/Syntax+Extensions.swift b/Macros/Implementations/Syntax+Extensions.swift index 890fe60..da224cc 100644 --- a/Macros/Implementations/Syntax+Extensions.swift +++ b/Macros/Implementations/Syntax+Extensions.swift @@ -4,6 +4,10 @@ import SwiftSyntaxMacros extension AttributeSyntax.Arguments { + func argumentValue(_ key: String) -> String? { + asLabeledExpressionList?.argumentValue(key) + } + func stringArgumentValue(_ key: String) -> String? { asLabeledExpressionList?.stringArgumentValue(key) } diff --git a/Macros/Module/Macros.swift b/Macros/Module/Macros.swift index 001fa48..87a4ea1 100644 --- a/Macros/Module/Macros.swift +++ b/Macros/Module/Macros.swift @@ -1,15 +1,28 @@ import Foundation -import SwiftSyntax -@freestanding(declaration, names: arbitrary) -public macro dayStringCodablePropertyWrapper(typeName: String, formatter: DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "CodableWrapperMacro") +// MARK: - Type macros + +@attached(member, names: arbitrary) +public macro DayStringBuiltin(name: String, formatter: DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "DayStringBuiltinPropertyWrapperMacro") +// Refactor these @freestanding(declaration, names: arbitrary) public macro epochCodablePropertyWrapper(typeName: String, milliseconds: Bool) = #externalMacro(module: "DayTypeMacroImplementations", type: "EpochWrapperMacro") @freestanding(declaration, names: arbitrary) public macro ios8601CodablePropertyWrapper(typeName: String, formatter: ISO8601DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "ISO8601WrapperMacro") +// MARK: - Common macros + @attached(member, names: named(decode), named(encode)) -public macro OptionalDayCodable(argumentName: String, argumentType: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "OptionalDayCodableMacro") +public macro OptionalDayCodableImplementation(argumentName: String, argumentType: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "OptionalDayCodableImplementationMacro") + +// MARK: - Keyed containers + +@attached(member, names: named(decode)) +public macro DecodeMissing(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerDecodeMissingMacro") + +@attached(member, names: named(encode)) +public macro EncodeMissing(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerEncodeMissingMacro") + diff --git a/Package.swift b/Package.swift index 9203cc6..d20c8fd 100755 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,6 @@ let package = Package( .target( name: "DayType", dependencies: [ - .product(name: "SwiftSyntax", package: "swift-syntax"), "DayTypeMacros", ], path: "Sources" @@ -28,7 +27,6 @@ let package = Package( .target( name: "DayTypeMacros", dependencies: [ - .product(name: "SwiftSyntax", package: "swift-syntax"), "DayTypeMacroImplementations", ], path: "Macros/Module" diff --git a/Sources/Property wrappers/DayString/DayString.swift b/Sources/Property wrappers/DayString/DayString.swift index efc3aca..bb08c59 100644 --- a/Sources/Property wrappers/DayString/DayString.swift +++ b/Sources/Property wrappers/DayString/DayString.swift @@ -1,10 +1,19 @@ -import Foundation import DayTypeMacros +import Foundation /// Identifies a ``Day`` property that reads and writes from day strings using a formatter. -public enum DayString where DayType: DayStringCodable { - #dayStringCodablePropertyWrapper(typeName: "DMY", formatter: DayFormatters.dmy) - #dayStringCodablePropertyWrapper(typeName: "MDY", formatter: DayFormatters.mdy) - #dayStringCodablePropertyWrapper(typeName: "YMD", formatter: DayFormatters.ymd) -} +@DayStringBuiltin(name: "DMY", formatter: DayFormatters.dmy) +@DayStringBuiltin(name: "MDY", formatter: DayFormatters.mdy) +@DayStringBuiltin(name: "YMD", formatter: DayFormatters.ymd) +public enum DayString where DayType: DayStringCodable {} + +@DecodeMissing(type: DayString.DMY.self) +@DecodeMissing(type: DayString.MDY.self) +@DecodeMissing(type: DayString.YMD.self) +extension KeyedDecodingContainer {} + +@EncodeMissing(type: DayString.DMY.self) +@EncodeMissing(type: DayString.MDY.self) +@EncodeMissing(type: DayString.YMD.self) +extension KeyedEncodingContainer {} diff --git a/Sources/Property wrappers/DayString/DayStringCodable.swift b/Sources/Property wrappers/DayString/DayStringCodable.swift index 378371d..022fe45 100644 --- a/Sources/Property wrappers/DayString/DayStringCodable.swift +++ b/Sources/Property wrappers/DayString/DayStringCodable.swift @@ -1,33 +1,42 @@ -import Foundation import DayTypeMacros +import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. /// /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol DayStringCodable { - static func decode(using decoder: Decoder, formatter: DateFormatter) throws -> Self - func encode(using encoder: Encoder, formatter: DateFormatter) throws + static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Self + func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls: Bool) throws } extension Day: DayStringCodable { - public static func decode(using decoder: Decoder, formatter: DateFormatter) throws -> Day { - let container = try decoder.singleValueContainer() + public static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Day { guard let dateString = try? container.decode(String.self), let date = formatter.date(from: dateString) else { - let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read the date string.") + let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Unable to read the date string.") throw DecodingError.dataCorrupted(context) } return Day(date: date) } - public func encode(using encoder: Encoder, formatter: DateFormatter) throws { - var container = encoder.singleValueContainer() + public func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls _: Bool) throws { try container.encode(formatter.string(from: date())) } } -///// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -@OptionalDayCodable(argumentName: "formatter", argumentType: DateFormatter.self) -extension Day?: DayStringCodable {} +/// `Day?` support which mostly just handles `nil`. +extension Day?: DayStringCodable { + public static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Day? { + container.decodeNil() ? nil : try Day.decode(from: container, formatter: formatter) + } + + public func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls: Bool) throws { + if let self { + try self.encode(into: &container, formatter: formatter, writeNulls: writeNulls) + } else if writeNulls { + try container.encodeNil() + } + } +} diff --git a/Sources/Property wrappers/Epoch/EpochCodable.swift b/Sources/Property wrappers/Epoch/EpochCodable.swift index 2e41d66..a4bc505 100644 --- a/Sources/Property wrappers/Epoch/EpochCodable.swift +++ b/Sources/Property wrappers/Epoch/EpochCodable.swift @@ -1,5 +1,5 @@ -import Foundation import DayTypeMacros +import Foundation /// Protocol that allows us to abstract the differences between ``Day`` and ``Day?``. /// @@ -7,7 +7,7 @@ import DayTypeMacros /// it erases the optional aspect of the values. public protocol EpochCodable { static func decode(using decoder: Decoder, milliseconds: Bool) throws -> Self - func encode(using encoder: Encoder, milliseconds: Bool) throws + func encode(using encoder: Encoder, milliseconds: Bool, writeNulls: Bool) throws } extension Day: EpochCodable { @@ -21,12 +21,26 @@ extension Day: EpochCodable { return Day(date: Date(timeIntervalSince1970: epochTime / (milliseconds ? 1000 : 1))) } - public func encode(using encoder: Encoder, milliseconds: Bool) throws { + public func encode(using encoder: Encoder, milliseconds: Bool, writeNulls _: Bool) throws { var container = encoder.singleValueContainer() try container.encode(date().timeIntervalSince1970 * (milliseconds ? 1000 : 1)) } } /// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -@OptionalDayCodable(argumentName: "milliseconds", argumentType: Bool.self) -extension Day?: EpochCodable {} +extension Day?: EpochCodable { + public static func decode(using decoder: Decoder, milliseconds: Bool) throws -> Day? { + let container = try decoder.singleValueContainer() + return container.decodeNil() ? nil : try Day.decode(using: decoder, milliseconds: milliseconds) + } + + public func encode(using encoder: Encoder, milliseconds: Bool, writeNulls: Bool) throws { + if let self { + try self.encode(using: encoder, milliseconds: milliseconds, writeNulls: writeNulls) + } else if writeNulls { + var container = encoder.singleValueContainer() + try container.encodeNil() + } + } + +} diff --git a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift index 101b4a7..581b389 100644 --- a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift +++ b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift @@ -7,7 +7,7 @@ import Foundation /// it erases the optional aspect of the values. public protocol ISO8601Codable { static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Self - func encode(using encoder: Encoder, formatter: ISO8601DateFormatter) throws + func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws } extension Day: ISO8601Codable { @@ -22,12 +22,25 @@ extension Day: ISO8601Codable { return Day(date: date) } - public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter) throws { + public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls _: Bool) throws { var container = encoder.singleValueContainer() try container.encode(formatter.string(from: date())) } } /// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. -@OptionalDayCodable(argumentName: "formatter", argumentType: ISO8601DateFormatter.self) -extension Day?: ISO8601Codable {} +extension Day?: ISO8601Codable { + public static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day? { + let container = try decoder.singleValueContainer() + return container.decodeNil() ? nil : try Day.decode(using: decoder, formatter: formatter) + } + + public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws { + if let self { + try self.encode(using: encoder, formatter: formatter, writeNulls: writeNulls) + } else if writeNulls { + var container = encoder.singleValueContainer() + try container.encodeNil() + } + } +} diff --git a/Tests/Property wrappers/CustomISO8601Tests.swift b/Tests/Property wrappers/CustomISO8601Tests.swift index ce0cd1d..5ce501a 100644 --- a/Tests/Property wrappers/CustomISO8601Tests.swift +++ b/Tests/Property wrappers/CustomISO8601Tests.swift @@ -1,105 +1,105 @@ -import DayType -import Foundation -import Testing - -private struct ISO8601CustomContainer: Codable where Configurator: CustomISO8601Configurator { - @CustomISO8601 var d1: Day - init(d1: Day) { - self.d1 = d1 - } -} - -private struct ISO8601CustomOptionalContainer: Codable where Configurator: CustomISO8601Configurator { - @CustomISO8601 var d1: Day? - init(d1: Day?) { - self.d1 = d1 - } -} - -extension PropertyWrapperSuites { - - @Suite("@CustomISO8601") - struct CustomISO8601Tests { - - @Test("Sans Timezone decoding") - func sansTimeZoneDecoding() throws { - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Local timezone decoding") - func sansTimeZoneToLocalTimeZoneDecoding() throws { - enum SansTimeZoneToMelbourneTimeZone: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - formatter.formatOptions.remove(.withTimeZone) - } - } - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 02)) - } - - @Test("Floating to a different timezone decoding") - func timeZoneOverridingDateTimeZoneDecoding() throws { - enum BrazilToMelbourneTimeZone: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - } - } - let json = #"{"d1": "2012-02-02T13:33:23-03:00"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Numeric iSO8601 decoding") - func minimalFormatDecoding() throws { - enum MinimalFormat: CustomISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) - formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) - } - } - let json = #"{"d1": "20120202 133323"}"# - let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 02)) - } - - @Test("Optional sans timezone decoding") - func optionalSansTimeZoneDecoding() throws { - let json = #"{"d1": "2012-02-02T13:33:23"}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Sans timezone decoding of nil") - func optionalSansTimeZoneWithNilDecoding() throws { - let json = #"{"d1":null}"# - let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } - - @Test("Custom ISO8601 encoding") - func encodingISO8601() throws { - let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) - } - - @Test("Optional Custom ISO8601 encoding") - func encodingToOptional() throws { - let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) - } - - @Test("Optional Custom ISO8601 encoding of nil") - func testEncodingNil() throws { - let instance = ISO8601CustomOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) - } - } -} +//import DayType +//import Foundation +//import Testing +// +//private struct ISO8601CustomContainer: Codable where Configurator: CustomISO8601Configurator { +// @CustomISO8601 var d1: Day +// init(d1: Day) { +// self.d1 = d1 +// } +//} +// +//private struct ISO8601CustomOptionalContainer: Codable where Configurator: CustomISO8601Configurator { +// @CustomISO8601 var d1: Day? +// init(d1: Day?) { +// self.d1 = d1 +// } +//} +// +//extension PropertyWrapperSuites { +// +// @Suite("@CustomISO8601") +// struct CustomISO8601Tests { +// +// @Test("Sans Timezone decoding") +// func sansTimeZoneDecoding() throws { +// let json = #"{"d1": "2012-02-02T13:33:23"}"# +// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Local timezone decoding") +// func sansTimeZoneToLocalTimeZoneDecoding() throws { +// enum SansTimeZoneToMelbourneTimeZone: CustomISO8601Configurator { +// static func configure(formatter: ISO8601DateFormatter) { +// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) +// formatter.formatOptions.remove(.withTimeZone) +// } +// } +// let json = #"{"d1": "2012-02-02T13:33:23"}"# +// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 02)) +// } +// +// @Test("Floating to a different timezone decoding") +// func timeZoneOverridingDateTimeZoneDecoding() throws { +// enum BrazilToMelbourneTimeZone: CustomISO8601Configurator { +// static func configure(formatter: ISO8601DateFormatter) { +// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) +// } +// } +// let json = #"{"d1": "2012-02-02T13:33:23-03:00"}"# +// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Numeric iSO8601 decoding") +// func minimalFormatDecoding() throws { +// enum MinimalFormat: CustomISO8601Configurator { +// static func configure(formatter: ISO8601DateFormatter) { +// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) +// formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) +// formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) +// } +// } +// let json = #"{"d1": "20120202 133323"}"# +// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 02)) +// } +// +// @Test("Optional sans timezone decoding") +// func optionalSansTimeZoneDecoding() throws { +// let json = #"{"d1": "2012-02-02T13:33:23"}"# +// let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Sans timezone decoding of nil") +// func optionalSansTimeZoneWithNilDecoding() throws { +// let json = #"{"d1":null}"# +// let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == nil) +// } +// +// @Test("Custom ISO8601 encoding") +// func encodingISO8601() throws { +// let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) +// } +// +// @Test("Optional Custom ISO8601 encoding") +// func encodingToOptional() throws { +// let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) +// } +// +// @Test("Optional Custom ISO8601 encoding of nil") +// func testEncodingNil() throws { +// let instance = ISO8601CustomOptionalContainer(d1: nil) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) +// } +// } +//} diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 4db08b9..2e03aaa 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -1,108 +1,128 @@ -//import DayType -//import Foundation -//import Testing -// -//private struct DayContainer: Codable where Configuration: DayStringConfiguration { -// @DayString var d1: Day -//} -// -//private struct OptionalDayContainer: Codable where Configuration: DayStringConfiguration { -// @DayString var d1: Day? -//} -// -//extension PropertyWrapperSuites { -// -// @Suite("@DayString") -// struct DateStringTests { -// -// @Test("ISO8601 date decoding") -// func decodingAnISO8601Date() throws { -// let json = #"{"d1": "2012-02-01"}"# -// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("DMY date decoding") -// func decodingADMYDate() throws { -// let json = #"{"d1": "01/02/2012"}"# -// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("MDY date decoding") -// func decodingAMDYDate() throws { -// let json = #"{"d1": "02/01/2012"}"# -// let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("Optional ISO8601 date decoding") -// func decodingAnIOptionalISO8601Date() throws { -// let json = #"{"d1": "2012-02-01"}"# -// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("DMY date") -// func decodingAnOptionalDMYDate() throws { -// let json = #"{"d1": "01/02/2012"}"# -// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("MDY date decoding") -// func decodingAnOptionalMDYDate() throws { -// let json = #"{"d1": "02/01/2012"}"# -// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 01)) -// } -// -// @Test("Nil date decoding") -// func decodingANilDate() throws { -// let json = #"{"d1": null}"# -// let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == nil) -// } -// -// @Test("Invalid date decoding throws an error") -// func decodingInvalidDateThrows() throws { -// do { -// let json = #"{"d1": "xxx"}"# -// _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.dataCorrupted(let context) { -// #expect(context.codingPath.map(\.stringValue) == ["d1"]) -// #expect(context.debugDescription == "Unable to read the date string.") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("DMY date encoding") -// func encodingDateString() throws { -// let instance = DayContainer(d1: Day(2012, 02, 01)) -// let encoder = JSONEncoder() -// encoder.outputFormatting = .withoutEscapingSlashes -// let result = try encoder.encode(instance) -// #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) -// } -// -// @Test("Optional DMY date encoding") -// func encodingOptionalDateString() throws { -// let instance = OptionalDayContainer(d1: Day(2012, 02, 01)) -// let encoder = JSONEncoder() -// encoder.outputFormatting = .withoutEscapingSlashes -// let result = try encoder.encode(instance) -// #expect(String(data: result, encoding: .utf8)! == #"{"d1":"01/02/2012"}"#) -// } -// -// @Test("Optional DMY date encoding from a nil") -// func encodingOptionalDateStringWithNil() throws { -// let instance = OptionalDayContainer(d1: nil) -// let encoder = JSONEncoder() -// encoder.outputFormatting = .withoutEscapingSlashes -// let result = try encoder.encode(instance) -// #expect(String(data: result, encoding: .utf8)! == #"{"d1":null}"#) -// } -// } -//} +import DayType +import Foundation +import Testing + +private struct DayContainer: Codable { + @DayString.DMY var dmy: Day + @DayString.MDY var mdy: Day + @DayString.YMD var ymd: Day +} + +private struct OptionalDayContainer: Codable { + @DayString.DMY var dmy: Day? + @DayString.MDY var mdy: Day? + @DayString.YMD var ymd: Day? +} + +private struct OptionalNilContainer: Codable { + @DayString.DMY var dmy: Day? + @DayString.MDY var mdy: Day? + @DayString.YMD var ymd: Day? +} + +private struct OptionalNullContainer: Codable { + @DayString.DMY.WritesNulls var dmy: Day? + @DayString.MDY.WritesNulls var mdy: Day? + @DayString.YMD.WritesNulls var ymd: Day? +} + +extension PropertyWrapperSuites { + + @Suite("@DayString") + struct DateStringTests { + + @Test("Decoding date strings") + func decodingDates() throws { + let json = #"{"dmy": "01/02/2012", "mdy": "02/01/2012", "ymd": "2012-02-01"}"# + let result = try JSONDecoder().decode(DayContainer.self, from: json.data(using: .utf8)!) + let expectedDate = Day(2012, 02, 01) + #expect(result.dmy == expectedDate) + #expect(result.mdy == expectedDate) + #expect(result.ymd == expectedDate) + } + + @Test("Optional date string decoding") + func decodingOptionalDates() throws { + let json = #"{"dmy": "01/02/2012", "mdy": "02/01/2012", "ymd": "2012-02-01"}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let expectedDate = Day(2012, 02, 01) + #expect(result.dmy == expectedDate) + #expect(result.mdy == expectedDate) + #expect(result.ymd == expectedDate) + } + + @Test("Optional nil decoding") + func decodingOptionalNilDates() throws { + let json = #"{"dmy": null, "mdy": null, "ymd": null}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.dmy == nil) + #expect(result.mdy == nil) + #expect(result.ymd == nil) + } + + @Test("Optional missing decoding") + func decodingOptionalMissingDates() throws { + let json = #"{}"# + let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + #expect(result.dmy == nil) + #expect(result.mdy == nil) + #expect(result.ymd == nil) + } + + @Test("Invalid date string decoding throws an error") + func decodingInvalidDateThrows() throws { + do { + let json = #"{"dmy": "xxx"}"# + _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.map(\.stringValue) == ["dmy"]) + #expect(context.debugDescription == "Unable to read the date string.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test("Date string encoding") + func encodingDateStrings() throws { + let day = Day(2012, 02, 01) + let instance = DayContainer(dmy: day, mdy: day, ymd: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"dmy":"01/02/2012","mdy":"02/01/2012","ymd":"2012-02-01"}"#) + } + + @Test("Optional date string encoding") + func encodingOptionalDateStrings() throws { + let day = Day(2012, 02, 01) + let instance = OptionalDayContainer(dmy: day, mdy: day, ymd: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"dmy":"01/02/2012","mdy":"02/01/2012","ymd":"2012-02-01"}"#) + } + + @Test("Optional date encoding nil") + func encodingOptionalDateStringsWithNil() throws { + let instance = OptionalDayContainer(dmy: nil, mdy: nil, ymd: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == "{}") + } + + @Test("Optional date encoding nil -> null") + func encodingOptionalDateStringsWithNilToNull() throws { + let instance = OptionalNullContainer(dmy: nil, mdy: nil, ymd: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"dmy":null,"mdy":null,"ymd":null}"#) + } + } +} diff --git a/Tests/Property wrappers/EpochMillisecondsTests.swift b/Tests/Property wrappers/EpochMillisecondsTests.swift index 471e698..c830f2a 100644 --- a/Tests/Property wrappers/EpochMillisecondsTests.swift +++ b/Tests/Property wrappers/EpochMillisecondsTests.swift @@ -1,93 +1,93 @@ -import DayType -import Foundation -import Testing - -private struct EpochContainer: Codable { - @EpochMilliseconds var d1: Day - init(d1: Day) { - self.d1 = d1 - } -} - -private struct EpochOptionalContainer: Codable { - @EpochMilliseconds var d1: Day? - init(d1: Day?) { - self.d1 = d1 - } -} - -extension PropertyWrapperSuites { - - @Suite("@EpochMilliseconds") - struct EpochMillisecondsTests { - - @Test("Decoding") - func decoding() throws { - let json = #"{"d1": 1328251182123}"# - let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding an invalid value") - func decodingInvalidValue() throws { - do { - let json = #"{"d1": "2012-02-03T10:33:23.123+11:00"}"# - _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - #expect(context.codingPath.last?.stringValue == "d1") - #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Optional decoding") - func optionalDecoding() throws { - let json = #"{"d1": 1328251182123}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Optional decoding nil") - func decodingNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } - - @Test("Optional decoding with a missing value") - func decodingWithMissingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - #expect(key.stringValue == "d1") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Encoding") - func encoding() throws { - let instance = EpochContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) - } - - @Test("Optional encoding") - func encodingOptional() throws { - let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) - } - - @Test("Optional encoding with a nil") - func encodingNil() throws { - let instance = EpochOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) - } - } -} +//import DayType +//import Foundation +//import Testing +// +//private struct EpochContainer: Codable { +// @EpochMilliseconds var d1: Day +// init(d1: Day) { +// self.d1 = d1 +// } +//} +// +//private struct EpochOptionalContainer: Codable { +// @EpochMilliseconds var d1: Day? +// init(d1: Day?) { +// self.d1 = d1 +// } +//} +// +//extension PropertyWrapperSuites { +// +// @Suite("@EpochMilliseconds") +// struct EpochMillisecondsTests { +// +// @Test("Decoding") +// func decoding() throws { +// let json = #"{"d1": 1328251182123}"# +// let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding an invalid value") +// func decodingInvalidValue() throws { +// do { +// let json = #"{"d1": "2012-02-03T10:33:23.123+11:00"}"# +// _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.dataCorrupted(let context) { +// #expect(context.codingPath.last?.stringValue == "d1") +// #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Optional decoding") +// func optionalDecoding() throws { +// let json = #"{"d1": 1328251182123}"# +// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Optional decoding nil") +// func decodingNil() throws { +// let json = #"{"d1": null}"# +// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == nil) +// } +// +// @Test("Optional decoding with a missing value") +// func decodingWithMissingValue() throws { +// do { +// let json = #"{}"# +// _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.keyNotFound(let key, _) { +// #expect(key.stringValue == "d1") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Encoding") +// func encoding() throws { +// let instance = EpochContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) +// } +// +// @Test("Optional encoding") +// func encodingOptional() throws { +// let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) +// } +// +// @Test("Optional encoding with a nil") +// func encodingNil() throws { +// let instance = EpochOptionalContainer(d1: nil) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) +// } +// } +//} diff --git a/Tests/Property wrappers/EpochSecondsTests.swift b/Tests/Property wrappers/EpochSecondsTests.swift index 60e5fb0..22af210 100644 --- a/Tests/Property wrappers/EpochSecondsTests.swift +++ b/Tests/Property wrappers/EpochSecondsTests.swift @@ -1,93 +1,93 @@ -import DayType -import Foundation -import Testing - -private struct EpochContainer: Codable { - @EpochSeconds var d1: Day - init(d1: Day) { - self.d1 = d1 - } -} - -private struct EpochOptionalContainer: Codable { - @EpochSeconds var d1: Day? - init(d1: Day?) { - self.d1 = d1 - } -} - -extension PropertyWrapperSuites { - - @Suite("@EpochSeconds") - struct EpochSecondsTests { - - @Test("Decoding") - func decoding() throws { - let json = #"{"d1": 1328251182}"# - let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding an invalid value") - func decodingWithInvalidValue() throws { - do { - let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# - _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.dataCorrupted(let context) { - #expect(context.codingPath.last?.stringValue == "d1") - #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Decoding an optional") - func decodingOptional() throws { - let json = #"{"d1": 1328251182}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == Day(2012, 02, 03)) - } - - @Test("Decoding an optional with a nil") - func decodingWithNil() throws { - let json = #"{"d1": null}"# - let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - #expect(result.d1 == nil) - } - - @Test("Decoding an optional when value is missing") - func missingValue() throws { - do { - let json = #"{}"# - _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) - Issue.record("Error not thrown") - } catch DecodingError.keyNotFound(let key, _) { - #expect(key.stringValue == "d1") - } catch { - Issue.record("Unexpected error: \(error)") - } - } - - @Test("Encoding epoch seconds") - func encoding() throws { - let instance = EpochContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) - } - - @Test("Encoding when optional") - func encodingOptional() throws { - let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) - } - - @Test("Encoding when ooptional and nil value") - func encodingOptionalWithNil() throws { - let instance = EpochOptionalContainer(d1: nil) - let result = try JSONEncoder().encode(instance) - #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) - } - } -} +//import DayType +//import Foundation +//import Testing +// +//private struct EpochContainer: Codable { +// @EpochSeconds var d1: Day +// init(d1: Day) { +// self.d1 = d1 +// } +//} +// +//private struct EpochOptionalContainer: Codable { +// @EpochSeconds var d1: Day? +// init(d1: Day?) { +// self.d1 = d1 +// } +//} +// +//extension PropertyWrapperSuites { +// +// @Suite("@EpochSeconds") +// struct EpochSecondsTests { +// +// @Test("Decoding") +// func decoding() throws { +// let json = #"{"d1": 1328251182}"# +// let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding an invalid value") +// func decodingWithInvalidValue() throws { +// do { +// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# +// _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.dataCorrupted(let context) { +// #expect(context.codingPath.last?.stringValue == "d1") +// #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Decoding an optional") +// func decodingOptional() throws { +// let json = #"{"d1": 1328251182}"# +// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == Day(2012, 02, 03)) +// } +// +// @Test("Decoding an optional with a nil") +// func decodingWithNil() throws { +// let json = #"{"d1": null}"# +// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// #expect(result.d1 == nil) +// } +// +// @Test("Decoding an optional when value is missing") +// func missingValue() throws { +// do { +// let json = #"{}"# +// _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) +// Issue.record("Error not thrown") +// } catch DecodingError.keyNotFound(let key, _) { +// #expect(key.stringValue == "d1") +// } catch { +// Issue.record("Unexpected error: \(error)") +// } +// } +// +// @Test("Encoding epoch seconds") +// func encoding() throws { +// let instance = EpochContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) +// } +// +// @Test("Encoding when optional") +// func encodingOptional() throws { +// let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) +// } +// +// @Test("Encoding when ooptional and nil value") +// func encodingOptionalWithNil() throws { +// let instance = EpochOptionalContainer(d1: nil) +// let result = try JSONEncoder().encode(instance) +// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) +// } +// } +//} diff --git a/Tests/Property wrappers/MacroTests.swift b/Tests/Property wrappers/MacroTests.swift deleted file mode 100644 index 02519d6..0000000 --- a/Tests/Property wrappers/MacroTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Testing -import DayType - -struct X { - - @DayString.DMY - var a: Day - - @DayString.DMY - var b: Day? - -} From 7fb5ca9e7fe5c68bb0aca34f3b628461d2250096 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 11 May 2025 19:52:58 +1000 Subject: [PATCH 23/26] Adjusting function calls. --- ...DayStringBuiltinPropertyWrapperMacro.swift | 5 ++--- .../DayString/DayStringCodable.swift | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift index 896b4f1..b59186c 100644 --- a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift +++ b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift @@ -46,12 +46,11 @@ struct DayStringBuiltinPropertyWrapperMacro: MemberMacro { } public init(from decoder: Decoder) throws { - wrappedValue = try DayType.decode(from: try decoder.singleValueContainer(), formatter: \(formatter)) + wrappedValue = try DayType.decode(from: try decoder, formatter: \(formatter)) } public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try wrappedValue.encode(into: &container, formatter: \(formatter), writeNulls: \(writeNulls)) + try wrappedValue.encode(into: encoder, formatter: \(formatter), writeNulls: \(writeNulls)) } } """ diff --git a/Sources/Property wrappers/DayString/DayStringCodable.swift b/Sources/Property wrappers/DayString/DayStringCodable.swift index 022fe45..6b63622 100644 --- a/Sources/Property wrappers/DayString/DayStringCodable.swift +++ b/Sources/Property wrappers/DayString/DayStringCodable.swift @@ -6,13 +6,14 @@ import Foundation /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol DayStringCodable { - static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Self - func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls: Bool) throws + static func decode(from decoder: Decoder, formatter: DateFormatter) throws -> Self + func encode(into encoder: Encoder, formatter: DateFormatter, writeNulls: Bool) throws } extension Day: DayStringCodable { - public static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Day { + public static func decode(from decoder: Decoder, formatter: DateFormatter) throws -> Day { + let container = try decoder.singleValueContainer() guard let dateString = try? container.decode(String.self), let date = formatter.date(from: dateString) else { let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Unable to read the date string.") @@ -21,21 +22,24 @@ extension Day: DayStringCodable { return Day(date: date) } - public func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls _: Bool) throws { + public func encode(into encoder: Encoder, formatter: DateFormatter, writeNulls _: Bool) throws { + var container = encoder.singleValueContainer() try container.encode(formatter.string(from: date())) } } /// `Day?` support which mostly just handles `nil`. extension Day?: DayStringCodable { - public static func decode(from container: SingleValueDecodingContainer, formatter: DateFormatter) throws -> Day? { - container.decodeNil() ? nil : try Day.decode(from: container, formatter: formatter) + public static func decode(from decoder: Decoder, formatter: DateFormatter) throws -> Day? { + let container = try decoder.singleValueContainer() + return container.decodeNil() ? nil : try Day.decode(from: decoder, formatter: formatter) } - public func encode(into container: inout SingleValueEncodingContainer, formatter: DateFormatter, writeNulls: Bool) throws { + public func encode(into encoder: Encoder, formatter: DateFormatter, writeNulls: Bool) throws { if let self { - try self.encode(into: &container, formatter: formatter, writeNulls: writeNulls) + try self.encode(into: encoder, formatter: formatter, writeNulls: writeNulls) } else if writeNulls { + var container = encoder.singleValueContainer() try container.encodeNil() } } From 58ff33494d12e1673edd81d2019841e47c923afc Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 12 May 2025 15:49:57 +1000 Subject: [PATCH 24/26] Adding new epoch implementation --- .../xcschemes/DayType-Package.xcscheme | 98 +++++++++++++++ .../xcschemes/DayTypeMacros.xcscheme | 67 ++++++++++ DayType-Package.xctestplan | 24 ++++ ...DayStringBuiltinPropertyWrapperMacro.swift | 7 +- .../EpochPropertyWrapperMacro.swift | 56 +++++++++ .../Implementations/EpochWrapperMacro.swift | 39 ------ ...ContainerDecodeMissingDayStringMacro.swift | 35 ++++++ ...yedContainerDecodeMissingEpochMacro.swift} | 2 +- ...ontainerEncodeMissingDayStringMacro.swift} | 4 +- ...eyedContainerEncodeMissingEpochMacro.swift | 40 ++++++ Macros/Implementations/Plugins.swift | 8 +- Macros/Module/Macros.swift | 16 ++- .../DayString/DayString.swift | 18 +-- Sources/Property wrappers/Epoch/Epoch.swift | 15 ++- Tests/Property wrappers/DateStringTests.swift | 30 ++--- .../EpochMillisecondsTests.swift | 93 -------------- .../Property wrappers/EpochSecondsTests.swift | 93 -------------- Tests/Property wrappers/EpochTests.swift | 115 ++++++++++++++++++ 18 files changed, 490 insertions(+), 270 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/DayType-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/DayTypeMacros.xcscheme create mode 100644 DayType-Package.xctestplan create mode 100644 Macros/Implementations/EpochPropertyWrapperMacro.swift delete mode 100644 Macros/Implementations/EpochWrapperMacro.swift create mode 100644 Macros/Implementations/KeyedContainerDecodeMissingDayStringMacro.swift rename Macros/Implementations/{KeyedContainerDecodeMissingMacro.swift => KeyedContainerDecodeMissingEpochMacro.swift} (95%) rename Macros/Implementations/{KeyedContainerEncodeMissingMacro.swift => KeyedContainerEncodeMissingDayStringMacro.swift} (90%) create mode 100644 Macros/Implementations/KeyedContainerEncodeMissingEpochMacro.swift delete mode 100644 Tests/Property wrappers/EpochMillisecondsTests.swift delete mode 100644 Tests/Property wrappers/EpochSecondsTests.swift create mode 100644 Tests/Property wrappers/EpochTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/DayType-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/DayType-Package.xcscheme new file mode 100644 index 0000000..e738e2a --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/DayType-Package.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/DayTypeMacros.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/DayTypeMacros.xcscheme new file mode 100644 index 0000000..38c35f3 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/DayTypeMacros.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DayType-Package.xctestplan b/DayType-Package.xctestplan new file mode 100644 index 0000000..4d86245 --- /dev/null +++ b/DayType-Package.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "F28B9C65-0AA9-4599-8E55-40AEEF5A8B41", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "DayTypeTests", + "name" : "DayTypeTests" + } + } + ], + "version" : 1 +} diff --git a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift index b59186c..f339a74 100644 --- a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift +++ b/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift @@ -31,12 +31,13 @@ struct DayStringBuiltinPropertyWrapperMacro: MemberMacro { private static func propertyWrapper(name: String, formatter: String, withNullableImplementation: Bool) -> String { let writeNulls = withNullableImplementation ? "false" : "true" return """ - @propertyWrapper + @propertyWrapper public struct \(name): Codable { - \(withNullableImplementation ? propertyWrapper(name: "WritesNulls", formatter: formatter, withNullableImplementation: false) : "") + \(withNullableImplementation ? propertyWrapper(name: "Nullable", formatter: formatter, withNullableImplementation: false) : "") public var wrappedValue: DayType + // We need to expose these values so keyed containers can access them. let formatter = \(formatter) let writeNulls = \(writeNulls) @@ -46,7 +47,7 @@ struct DayStringBuiltinPropertyWrapperMacro: MemberMacro { } public init(from decoder: Decoder) throws { - wrappedValue = try DayType.decode(from: try decoder, formatter: \(formatter)) + wrappedValue = try DayType.decode(from: decoder, formatter: \(formatter)) } public func encode(to encoder: Encoder) throws { diff --git a/Macros/Implementations/EpochPropertyWrapperMacro.swift b/Macros/Implementations/EpochPropertyWrapperMacro.swift new file mode 100644 index 0000000..6ff5f69 --- /dev/null +++ b/Macros/Implementations/EpochPropertyWrapperMacro.swift @@ -0,0 +1,56 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct EpochPropertyWrapperMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf _: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard let typeName = node.arguments?.stringArgumentValue("typeName"), + let milliseconds = node.arguments?.argumentValue("milliseconds") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("typeName or milliseconds argument not supplied. Passed arguments \(node.arguments?.description ?? "")") + )) + return [] + } + + let withNullableImplementation = (node.arguments?.argumentValue("withNullableImplementation") ?? "true") == "true" + + return [ + DeclSyntax(stringLiteral: propertyWrapper(name: typeName, milliseconds: milliseconds, withNullableImplementation: withNullableImplementation)), + ] + } + + private static func propertyWrapper(name: String, milliseconds: String, withNullableImplementation: Bool) -> String { + let writeNulls = withNullableImplementation ? "false" : "true" + return """ + @propertyWrapper + public struct \(name): Codable { + + \(withNullableImplementation ? propertyWrapper(name: "Nullable", milliseconds: milliseconds, withNullableImplementation: false) : "") + + public var wrappedValue: DayType + + // We need to expose these values so keyed containers can access them. + let writeNulls = \(writeNulls) + let milliseconds = \(milliseconds) + + public init(wrappedValue: DayType) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + wrappedValue = try DayType.decode(using: decoder, milliseconds: \(milliseconds)) + } + + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(using: encoder, milliseconds: \(milliseconds), writeNulls: writeNulls) + } + } + """ + } +} diff --git a/Macros/Implementations/EpochWrapperMacro.swift b/Macros/Implementations/EpochWrapperMacro.swift deleted file mode 100644 index fe8d9ce..0000000 --- a/Macros/Implementations/EpochWrapperMacro.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxMacros - -struct EpochWrapperMacro: DeclarationMacro { - - static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { - - guard let typeName = node.arguments.stringArgumentValue("typeName"), - let milliseconds = node.arguments.argumentValue("milliseconds") else { - context.diagnose(Diagnostic( - node: node, message: MacroExpansionErrorMessage("typeName or milliseconds argument not supplied. Passed arguments \(node.arguments)") - )) - return [] - } - - return [ - """ - @propertyWrapper - public struct \(raw: typeName): Codable { - - public var wrappedValue: DayType - - public init(wrappedValue: DayType) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try DayType.decode(using: decoder, milliseconds: \(raw: milliseconds)) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(using: encoder, milliseconds: \(raw: milliseconds), writeNulls: false) - } - } - """, - ] - } -} diff --git a/Macros/Implementations/KeyedContainerDecodeMissingDayStringMacro.swift b/Macros/Implementations/KeyedContainerDecodeMissingDayStringMacro.swift new file mode 100644 index 0000000..19410b7 --- /dev/null +++ b/Macros/Implementations/KeyedContainerDecodeMissingDayStringMacro.swift @@ -0,0 +1,35 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct KeyedContainerDecodeMissingDayStringMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf owningType: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription == "KeyedDecodingContainer" else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Can only be used on a KeyedDecodingContainer: \(owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription ?? "")") + )) + return [] + } + + guard let decodableType = node.arguments?.typeArgumentValue("type") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Unable to determine passed type argument from: \(node.arguments?.description ?? "")") + )) + return [] + } + + return [ + """ + public func decode(_ type: \(raw: decodableType).Type, forKey key: Key) throws -> \(raw: decodableType) { + try decodeIfPresent(type, forKey: key) ?? .init(wrappedValue: nil) + } + """, + ] + } +} diff --git a/Macros/Implementations/KeyedContainerDecodeMissingMacro.swift b/Macros/Implementations/KeyedContainerDecodeMissingEpochMacro.swift similarity index 95% rename from Macros/Implementations/KeyedContainerDecodeMissingMacro.swift rename to Macros/Implementations/KeyedContainerDecodeMissingEpochMacro.swift index 2331c8a..7428f32 100644 --- a/Macros/Implementations/KeyedContainerDecodeMissingMacro.swift +++ b/Macros/Implementations/KeyedContainerDecodeMissingEpochMacro.swift @@ -2,7 +2,7 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -struct KeyedContainerDecodeMissingMacro: MemberMacro { +struct KeyedContainerDecodeMissingEpochMacro: MemberMacro { static func expansion( of node: AttributeSyntax, diff --git a/Macros/Implementations/KeyedContainerEncodeMissingMacro.swift b/Macros/Implementations/KeyedContainerEncodeMissingDayStringMacro.swift similarity index 90% rename from Macros/Implementations/KeyedContainerEncodeMissingMacro.swift rename to Macros/Implementations/KeyedContainerEncodeMissingDayStringMacro.swift index 3d0d37a..6b67a57 100644 --- a/Macros/Implementations/KeyedContainerEncodeMissingMacro.swift +++ b/Macros/Implementations/KeyedContainerEncodeMissingDayStringMacro.swift @@ -2,7 +2,7 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -struct KeyedContainerEncodeMissingMacro: MemberMacro { +struct KeyedContainerEncodeMissingDayStringMacro: MemberMacro { static func expansion( of node: AttributeSyntax, @@ -26,7 +26,7 @@ struct KeyedContainerEncodeMissingMacro: MemberMacro { return [ """ - /// Encioding must be handled by the keyed container or we end up writing keys with missing values. + /// Encoding must be handled by the keyed container or we end up writing keys with missing values. public mutating func encode(_ propertyWrapper: \(raw: encodableType), forKey key: Key) throws { // If there is a value then use the property wrappers to convert it to a string and write that. if let value = propertyWrapper.wrappedValue { diff --git a/Macros/Implementations/KeyedContainerEncodeMissingEpochMacro.swift b/Macros/Implementations/KeyedContainerEncodeMissingEpochMacro.swift new file mode 100644 index 0000000..30d8247 --- /dev/null +++ b/Macros/Implementations/KeyedContainerEncodeMissingEpochMacro.swift @@ -0,0 +1,40 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +struct KeyedContainerEncodeMissingEpochMacro: MemberMacro { + + static func expansion( + of node: AttributeSyntax, + providingMembersOf owningType: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + guard owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription == "KeyedEncodingContainer" else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Can only be used on a KeyedEncodingContainer: \(owningType.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription ?? "")") + )) + return [] + } + + guard let encodableType = node.arguments?.typeArgumentValue("type") else { + context.diagnose(Diagnostic( + node: node, message: MacroExpansionErrorMessage("Unable to determine passed type argument from: \(node.arguments?.description ?? "")") + )) + return [] + } + + return [ + """ + /// Encoding must be handled by the keyed container or we end up writing keys with missing values. + public mutating func encode(_ propertyWrapper: \(raw: encodableType), forKey key: Key) throws { + if let value = propertyWrapper.wrappedValue { + try self.encode(value.date().timeIntervalSince1970 * (propertyWrapper.milliseconds ? 1000 : 1), forKey: key) + } else if propertyWrapper.writeNulls { + try self.encodeNil(forKey: key) + } + } + """, + ] + } +} diff --git a/Macros/Implementations/Plugins.swift b/Macros/Implementations/Plugins.swift index 1705409..8834d3a 100644 --- a/Macros/Implementations/Plugins.swift +++ b/Macros/Implementations/Plugins.swift @@ -5,9 +5,11 @@ import SwiftSyntaxMacros struct MyMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ DayStringBuiltinPropertyWrapperMacro.self, - EpochWrapperMacro.self, + EpochPropertyWrapperMacro.self, ISO8601WrapperMacro.self, - KeyedContainerDecodeMissingMacro.self, - KeyedContainerEncodeMissingMacro.self + KeyedContainerDecodeMissingDayStringMacro.self, + KeyedContainerEncodeMissingDayStringMacro.self, + KeyedContainerDecodeMissingEpochMacro.self, + KeyedContainerEncodeMissingEpochMacro.self, ] } diff --git a/Macros/Module/Macros.swift b/Macros/Module/Macros.swift index 87a4ea1..d44580b 100644 --- a/Macros/Module/Macros.swift +++ b/Macros/Module/Macros.swift @@ -3,11 +3,11 @@ import Foundation // MARK: - Type macros @attached(member, names: arbitrary) -public macro DayStringBuiltin(name: String, formatter: DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "DayStringBuiltinPropertyWrapperMacro") +public macro DayStringPropertyWrapper(name: String, formatter: DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "DayStringBuiltinPropertyWrapperMacro") // Refactor these -@freestanding(declaration, names: arbitrary) -public macro epochCodablePropertyWrapper(typeName: String, milliseconds: Bool) = #externalMacro(module: "DayTypeMacroImplementations", type: "EpochWrapperMacro") +@attached(member, names: arbitrary) +public macro EpochPropertyWrapper(typeName: String, milliseconds: Bool, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "EpochPropertyWrapperMacro") @freestanding(declaration, names: arbitrary) public macro ios8601CodablePropertyWrapper(typeName: String, formatter: ISO8601DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "ISO8601WrapperMacro") @@ -20,9 +20,15 @@ public macro OptionalDayCodableImplementation(argumentName: String, argumentType // MARK: - Keyed containers @attached(member, names: named(decode)) -public macro DecodeMissing(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerDecodeMissingMacro") +public macro DecodeMissingString(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerDecodeMissingDayStringMacro") + +@attached(member, names: named(encode)) +public macro EncodeMissingString(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerEncodeMissingDayStringMacro") + +@attached(member, names: named(decode)) +public macro DecodeMissingEpoch(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerDecodeMissingEpochMacro") @attached(member, names: named(encode)) -public macro EncodeMissing(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerEncodeMissingMacro") +public macro EncodeMissingEpoch(type: Any.Type) = #externalMacro(module: "DayTypeMacroImplementations", type: "KeyedContainerEncodeMissingEpochMacro") diff --git a/Sources/Property wrappers/DayString/DayString.swift b/Sources/Property wrappers/DayString/DayString.swift index bb08c59..34ed45e 100644 --- a/Sources/Property wrappers/DayString/DayString.swift +++ b/Sources/Property wrappers/DayString/DayString.swift @@ -3,17 +3,17 @@ import Foundation /// Identifies a ``Day`` property that reads and writes from day strings using a formatter. -@DayStringBuiltin(name: "DMY", formatter: DayFormatters.dmy) -@DayStringBuiltin(name: "MDY", formatter: DayFormatters.mdy) -@DayStringBuiltin(name: "YMD", formatter: DayFormatters.ymd) +@DayStringPropertyWrapper(name: "DMY", formatter: DayFormatters.dmy) +@DayStringPropertyWrapper(name: "MDY", formatter: DayFormatters.mdy) +@DayStringPropertyWrapper(name: "YMD", formatter: DayFormatters.ymd) public enum DayString where DayType: DayStringCodable {} -@DecodeMissing(type: DayString.DMY.self) -@DecodeMissing(type: DayString.MDY.self) -@DecodeMissing(type: DayString.YMD.self) +@DecodeMissingString(type: DayString.DMY.self) +@DecodeMissingString(type: DayString.MDY.self) +@DecodeMissingString(type: DayString.YMD.self) extension KeyedDecodingContainer {} -@EncodeMissing(type: DayString.DMY.self) -@EncodeMissing(type: DayString.MDY.self) -@EncodeMissing(type: DayString.YMD.self) +@EncodeMissingString(type: DayString.DMY.self) +@EncodeMissingString(type: DayString.MDY.self) +@EncodeMissingString(type: DayString.YMD.self) extension KeyedEncodingContainer {} diff --git a/Sources/Property wrappers/Epoch/Epoch.swift b/Sources/Property wrappers/Epoch/Epoch.swift index f8f2700..d25370d 100644 --- a/Sources/Property wrappers/Epoch/Epoch.swift +++ b/Sources/Property wrappers/Epoch/Epoch.swift @@ -1,7 +1,14 @@ import Foundation import DayTypeMacros -public enum Epoch where DayType: EpochCodable { - #epochCodablePropertyWrapper(typeName: "Milliseconds", milliseconds: true) - #epochCodablePropertyWrapper(typeName: "Seconds", milliseconds: false) -} +@EpochPropertyWrapper(typeName: "Milliseconds", milliseconds: true) +@EpochPropertyWrapper(typeName: "Seconds", milliseconds: false) +public enum Epoch where DayType: EpochCodable {} + +@DecodeMissingEpoch(type: Epoch.Milliseconds.self) +@DecodeMissingEpoch(type: Epoch.Seconds.self) +extension KeyedDecodingContainer {} + +@EncodeMissingEpoch(type: Epoch.Milliseconds.self) +@EncodeMissingEpoch(type: Epoch.Seconds.self) +extension KeyedEncodingContainer {} diff --git a/Tests/Property wrappers/DateStringTests.swift b/Tests/Property wrappers/DateStringTests.swift index 2e03aaa..0af42fd 100644 --- a/Tests/Property wrappers/DateStringTests.swift +++ b/Tests/Property wrappers/DateStringTests.swift @@ -8,22 +8,16 @@ private struct DayContainer: Codable { @DayString.YMD var ymd: Day } -private struct OptionalDayContainer: Codable { +private struct DayOptionalContainer: Codable { @DayString.DMY var dmy: Day? @DayString.MDY var mdy: Day? @DayString.YMD var ymd: Day? } -private struct OptionalNilContainer: Codable { - @DayString.DMY var dmy: Day? - @DayString.MDY var mdy: Day? - @DayString.YMD var ymd: Day? -} - -private struct OptionalNullContainer: Codable { - @DayString.DMY.WritesNulls var dmy: Day? - @DayString.MDY.WritesNulls var mdy: Day? - @DayString.YMD.WritesNulls var ymd: Day? +private struct DayOptionalNullContainer: Codable { + @DayString.DMY.Nullable var dmy: Day? + @DayString.MDY.Nullable var mdy: Day? + @DayString.YMD.Nullable var ymd: Day? } extension PropertyWrapperSuites { @@ -44,7 +38,7 @@ extension PropertyWrapperSuites { @Test("Optional date string decoding") func decodingOptionalDates() throws { let json = #"{"dmy": "01/02/2012", "mdy": "02/01/2012", "ymd": "2012-02-01"}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayOptionalContainer.self, from: json.data(using: .utf8)!) let expectedDate = Day(2012, 02, 01) #expect(result.dmy == expectedDate) #expect(result.mdy == expectedDate) @@ -54,7 +48,7 @@ extension PropertyWrapperSuites { @Test("Optional nil decoding") func decodingOptionalNilDates() throws { let json = #"{"dmy": null, "mdy": null, "ymd": null}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayOptionalContainer.self, from: json.data(using: .utf8)!) #expect(result.dmy == nil) #expect(result.mdy == nil) #expect(result.ymd == nil) @@ -63,7 +57,7 @@ extension PropertyWrapperSuites { @Test("Optional missing decoding") func decodingOptionalMissingDates() throws { let json = #"{}"# - let result = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + let result = try JSONDecoder().decode(DayOptionalContainer.self, from: json.data(using: .utf8)!) #expect(result.dmy == nil) #expect(result.mdy == nil) #expect(result.ymd == nil) @@ -73,7 +67,7 @@ extension PropertyWrapperSuites { func decodingInvalidDateThrows() throws { do { let json = #"{"dmy": "xxx"}"# - _ = try JSONDecoder().decode(OptionalDayContainer.self, from: json.data(using: .utf8)!) + _ = try JSONDecoder().decode(DayOptionalContainer.self, from: json.data(using: .utf8)!) Issue.record("Error not thrown") } catch DecodingError.dataCorrupted(let context) { #expect(context.codingPath.map(\.stringValue) == ["dmy"]) @@ -97,7 +91,7 @@ extension PropertyWrapperSuites { @Test("Optional date string encoding") func encodingOptionalDateStrings() throws { let day = Day(2012, 02, 01) - let instance = OptionalDayContainer(dmy: day, mdy: day, ymd: day) + let instance = DayOptionalContainer(dmy: day, mdy: day, ymd: day) let encoder = JSONEncoder() encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] let result = try encoder.encode(instance) @@ -107,7 +101,7 @@ extension PropertyWrapperSuites { @Test("Optional date encoding nil") func encodingOptionalDateStringsWithNil() throws { - let instance = OptionalDayContainer(dmy: nil, mdy: nil, ymd: nil) + let instance = DayOptionalContainer(dmy: nil, mdy: nil, ymd: nil) let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let result = try encoder.encode(instance) @@ -117,7 +111,7 @@ extension PropertyWrapperSuites { @Test("Optional date encoding nil -> null") func encodingOptionalDateStringsWithNilToNull() throws { - let instance = OptionalNullContainer(dmy: nil, mdy: nil, ymd: nil) + let instance = DayOptionalNullContainer(dmy: nil, mdy: nil, ymd: nil) let encoder = JSONEncoder() encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] let result = try encoder.encode(instance) diff --git a/Tests/Property wrappers/EpochMillisecondsTests.swift b/Tests/Property wrappers/EpochMillisecondsTests.swift deleted file mode 100644 index c830f2a..0000000 --- a/Tests/Property wrappers/EpochMillisecondsTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -//import DayType -//import Foundation -//import Testing -// -//private struct EpochContainer: Codable { -// @EpochMilliseconds var d1: Day -// init(d1: Day) { -// self.d1 = d1 -// } -//} -// -//private struct EpochOptionalContainer: Codable { -// @EpochMilliseconds var d1: Day? -// init(d1: Day?) { -// self.d1 = d1 -// } -//} -// -//extension PropertyWrapperSuites { -// -// @Suite("@EpochMilliseconds") -// struct EpochMillisecondsTests { -// -// @Test("Decoding") -// func decoding() throws { -// let json = #"{"d1": 1328251182123}"# -// let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding an invalid value") -// func decodingInvalidValue() throws { -// do { -// let json = #"{"d1": "2012-02-03T10:33:23.123+11:00"}"# -// _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.dataCorrupted(let context) { -// #expect(context.codingPath.last?.stringValue == "d1") -// #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Optional decoding") -// func optionalDecoding() throws { -// let json = #"{"d1": 1328251182123}"# -// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Optional decoding nil") -// func decodingNil() throws { -// let json = #"{"d1": null}"# -// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == nil) -// } -// -// @Test("Optional decoding with a missing value") -// func decodingWithMissingValue() throws { -// do { -// let json = #"{}"# -// _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.keyNotFound(let key, _) { -// #expect(key.stringValue == "d1") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Encoding") -// func encoding() throws { -// let instance = EpochContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) -// } -// -// @Test("Optional encoding") -// func encodingOptional() throws { -// let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600000}"#) -// } -// -// @Test("Optional encoding with a nil") -// func encodingNil() throws { -// let instance = EpochOptionalContainer(d1: nil) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) -// } -// } -//} diff --git a/Tests/Property wrappers/EpochSecondsTests.swift b/Tests/Property wrappers/EpochSecondsTests.swift deleted file mode 100644 index 22af210..0000000 --- a/Tests/Property wrappers/EpochSecondsTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -//import DayType -//import Foundation -//import Testing -// -//private struct EpochContainer: Codable { -// @EpochSeconds var d1: Day -// init(d1: Day) { -// self.d1 = d1 -// } -//} -// -//private struct EpochOptionalContainer: Codable { -// @EpochSeconds var d1: Day? -// init(d1: Day?) { -// self.d1 = d1 -// } -//} -// -//extension PropertyWrapperSuites { -// -// @Suite("@EpochSeconds") -// struct EpochSecondsTests { -// -// @Test("Decoding") -// func decoding() throws { -// let json = #"{"d1": 1328251182}"# -// let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding an invalid value") -// func decodingWithInvalidValue() throws { -// do { -// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# -// _ = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.dataCorrupted(let context) { -// #expect(context.codingPath.last?.stringValue == "d1") -// #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Decoding an optional") -// func decodingOptional() throws { -// let json = #"{"d1": 1328251182}"# -// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding an optional with a nil") -// func decodingWithNil() throws { -// let json = #"{"d1": null}"# -// let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == nil) -// } -// -// @Test("Decoding an optional when value is missing") -// func missingValue() throws { -// do { -// let json = #"{}"# -// _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.keyNotFound(let key, _) { -// #expect(key.stringValue == "d1") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Encoding epoch seconds") -// func encoding() throws { -// let instance = EpochContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) -// } -// -// @Test("Encoding when optional") -// func encodingOptional() throws { -// let instance = EpochOptionalContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":1328187600}"#) -// } -// -// @Test("Encoding when ooptional and nil value") -// func encodingOptionalWithNil() throws { -// let instance = EpochOptionalContainer(d1: nil) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) -// } -// } -//} diff --git a/Tests/Property wrappers/EpochTests.swift b/Tests/Property wrappers/EpochTests.swift new file mode 100644 index 0000000..bb93993 --- /dev/null +++ b/Tests/Property wrappers/EpochTests.swift @@ -0,0 +1,115 @@ +import DayType +import Foundation +import Testing + +private struct EpochContainer: Codable { + @Epoch.Milliseconds var milliseconds: Day + @Epoch.Seconds var seconds: Day +} + +private struct EpochOptionalContainer: Codable { + @Epoch.Milliseconds var milliseconds: Day? + @Epoch.Seconds var seconds: Day? +} + +private struct EpochOptionalNullContainer: Codable { + @Epoch.Milliseconds.Nullable var milliseconds: Day? + @Epoch.Seconds.Nullable var seconds: Day? +} + +extension PropertyWrapperSuites { + + @Suite("@Epoch") + struct EpochTests { + + @Test("Decoding epochs") + func decoding() throws { + let json = #"{"milliseconds": 1328251182123, "seconds": 1328251182}"# + let result = try JSONDecoder().decode(EpochContainer.self, from: json.data(using: .utf8)!) + let expectedDay = Day(2012, 02, 03) + #expect(result.milliseconds == expectedDay) + #expect(result.seconds == expectedDay) + } + + @Test("Decoding optional epochs") + func decodingOptional() throws { + let json = #"{"milliseconds": 1328251182123, "seconds": 1328251182}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + let expectedDay = Day(2012, 02, 03) + #expect(result.milliseconds == expectedDay) + #expect(result.seconds == expectedDay) + } + + @Test("Decoding missing epochs") + func decodingMissingOptional() throws { + let json = #"{}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.milliseconds == nil) + #expect(result.seconds == nil) + } + + @Test("Decoding null epochs") + func decodingNullOptional() throws { + let json = #"{"milliseconds": null, "seconds": null}"# + let result = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.milliseconds == nil) + #expect(result.seconds == nil) + } + + @Test("Decoding invalid epochs") + func decodingInvalidEpochs() throws { + do { + let json = #"{"milliseconds": "xxx"}"# + _ = try JSONDecoder().decode(EpochOptionalContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.map(\.stringValue) == ["milliseconds"]) + #expect(context.debugDescription == "Unable to read a Day value, expected an epoch.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test("Epoch encoding") + func encodingEpochs() throws { + let day = Day(2012, 02, 03) + let instance = EpochContainer(milliseconds: day, seconds: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"milliseconds":1328187600000,"seconds":1328187600}"#) + } + + @Test("Optional epoch encoding") + func optionalEncodingEpochs() throws { + let day = Day(2012, 02, 03) + let instance = EpochOptionalContainer(milliseconds: day, seconds: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"milliseconds":1328187600000,"seconds":1328187600}"#) + } + + @Test("Optional epoch encoding nil") + func optionalEncodingEpochsWithNil() throws { + let instance = EpochOptionalContainer(milliseconds: nil, seconds: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{}"#) + } + + @Test("Optional epoch encoding nil -> null") + func optionalEncodingEpochsWithNilToNull() throws { + let instance = EpochOptionalNullContainer(milliseconds: nil, seconds: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"milliseconds":null,"seconds":null}"#) + } + } +} From 14f77073268dabd3950e5604fb73621d083425b9 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Mon, 12 May 2025 22:38:54 +1000 Subject: [PATCH 25/26] ISO8601 wrappers --- ...FormattedStringPropertyWrapperMacro.swift} | 2 +- .../Implementations/ISO8601WrapperMacro.swift | 41 ---- Macros/Implementations/Plugins.swift | 3 +- Macros/Module/Macros.swift | 8 +- .../Property wrappers/ISO8601/ISO8601.swift | 17 +- .../ISO8601/ISO8601Codable.swift | 24 +- .../CustomISO8601Tests.swift | 105 -------- Tests/Property wrappers/ISO8601Tests.swift | 231 ++++++++++-------- 8 files changed, 160 insertions(+), 271 deletions(-) rename Macros/Implementations/{DayStringBuiltinPropertyWrapperMacro.swift => FormattedStringPropertyWrapperMacro.swift} (97%) delete mode 100644 Macros/Implementations/ISO8601WrapperMacro.swift delete mode 100644 Tests/Property wrappers/CustomISO8601Tests.swift diff --git a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift b/Macros/Implementations/FormattedStringPropertyWrapperMacro.swift similarity index 97% rename from Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift rename to Macros/Implementations/FormattedStringPropertyWrapperMacro.swift index f339a74..c01ccaf 100644 --- a/Macros/Implementations/DayStringBuiltinPropertyWrapperMacro.swift +++ b/Macros/Implementations/FormattedStringPropertyWrapperMacro.swift @@ -4,7 +4,7 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros /// Adds a static embedeed type for encoding and decoding using a passed formatter. -struct DayStringBuiltinPropertyWrapperMacro: MemberMacro { +struct FormattedStringPropertyWrapperMacro: MemberMacro { static func expansion( of node: AttributeSyntax, diff --git a/Macros/Implementations/ISO8601WrapperMacro.swift b/Macros/Implementations/ISO8601WrapperMacro.swift deleted file mode 100644 index c95fb51..0000000 --- a/Macros/Implementations/ISO8601WrapperMacro.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxMacros - -struct ISO8601WrapperMacro: DeclarationMacro { - - static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { - - guard let typeName = node.arguments.stringArgumentValue("typeName"), - let formatter = node.arguments.argumentValue("formatter") else { - context.diagnose(Diagnostic( - node: node, message: MacroExpansionErrorMessage("typeName or formatter argument not supplied. Passed arguments \(node.arguments)") - )) - return [] - } - - return [ - """ - @propertyWrapper - public struct \(raw: typeName): Codable { - - public var wrappedValue: DayType - // We need the formatter so that optional property wrappers can write day strings. - var formatter = \(raw: formatter) - - public init(wrappedValue: DayType) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - wrappedValue = try DayType.decode(using: decoder, formatter: \(raw: formatter)) - } - - public func encode(to encoder: Encoder) throws { - try wrappedValue.encode(using: encoder, formatter: \(raw: formatter), writeNulls: false) - } - } - """, - ] - } -} diff --git a/Macros/Implementations/Plugins.swift b/Macros/Implementations/Plugins.swift index 8834d3a..e6d5d32 100644 --- a/Macros/Implementations/Plugins.swift +++ b/Macros/Implementations/Plugins.swift @@ -4,9 +4,8 @@ import SwiftSyntaxMacros @main struct MyMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - DayStringBuiltinPropertyWrapperMacro.self, + FormattedStringPropertyWrapperMacro.self, EpochPropertyWrapperMacro.self, - ISO8601WrapperMacro.self, KeyedContainerDecodeMissingDayStringMacro.self, KeyedContainerEncodeMissingDayStringMacro.self, KeyedContainerDecodeMissingEpochMacro.self, diff --git a/Macros/Module/Macros.swift b/Macros/Module/Macros.swift index d44580b..4e73e21 100644 --- a/Macros/Module/Macros.swift +++ b/Macros/Module/Macros.swift @@ -3,15 +3,15 @@ import Foundation // MARK: - Type macros @attached(member, names: arbitrary) -public macro DayStringPropertyWrapper(name: String, formatter: DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "DayStringBuiltinPropertyWrapperMacro") +public macro DayStringPropertyWrapper(name: String, formatter: DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "FormattedStringPropertyWrapperMacro") + +@attached(member, names: arbitrary) +public macro ISO8601PropertyWrapper(name: String, formatter: ISO8601DateFormatter, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "FormattedStringPropertyWrapperMacro") // Refactor these @attached(member, names: arbitrary) public macro EpochPropertyWrapper(typeName: String, milliseconds: Bool, withNullableImplementation: Bool = true) = #externalMacro(module: "DayTypeMacroImplementations", type: "EpochPropertyWrapperMacro") -@freestanding(declaration, names: arbitrary) -public macro ios8601CodablePropertyWrapper(typeName: String, formatter: ISO8601DateFormatter) = #externalMacro(module: "DayTypeMacroImplementations", type: "ISO8601WrapperMacro") - // MARK: - Common macros @attached(member, names: named(decode), named(encode)) diff --git a/Sources/Property wrappers/ISO8601/ISO8601.swift b/Sources/Property wrappers/ISO8601/ISO8601.swift index dd1b345..a46bd26 100644 --- a/Sources/Property wrappers/ISO8601/ISO8601.swift +++ b/Sources/Property wrappers/ISO8601/ISO8601.swift @@ -1,7 +1,14 @@ -import Foundation import DayTypeMacros +import Foundation + +@ISO8601PropertyWrapper(name: "Default", formatter: DayFormatters.iso8601) +@ISO8601PropertyWrapper(name: "SansTimezone", formatter: DayFormatters.iso8601SansTimezone) +public enum ISO8601 where DayType: ISO8601Codable {} + +@DecodeMissingString(type: ISO8601.Default.self) +@DecodeMissingString(type: ISO8601.SansTimezone.self) +extension KeyedDecodingContainer {} -public enum ISO8601 where DayType: ISO8601Codable { - #ios8601CodablePropertyWrapper(typeName: "Default", formatter: DayFormatters.iso8601) - #ios8601CodablePropertyWrapper(typeName: "SansTimezone", formatter: DayFormatters.iso8601SansTimezone) -} +@EncodeMissingString(type: ISO8601.Default.self) +@EncodeMissingString(type: ISO8601.SansTimezone.self) +extension KeyedEncodingContainer {} diff --git a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift index 581b389..b1d4c06 100644 --- a/Sources/Property wrappers/ISO8601/ISO8601Codable.swift +++ b/Sources/Property wrappers/ISO8601/ISO8601Codable.swift @@ -6,38 +6,38 @@ import Foundation /// By using this protocols for property wrappers we can reduce the number of wrappers needed because /// it erases the optional aspect of the values. public protocol ISO8601Codable { - static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Self - func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws + static func decode(from decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Self + func encode(into encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws } extension Day: ISO8601Codable { - public static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day { + public static func decode(from decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day { let container = try decoder.singleValueContainer() - guard let iso8601String = try? container.decode(String.self), - let date = formatter.date(from: iso8601String) else { - let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to read a Day value, expected a valid ISO8601 string.") + guard let dateString = try? container.decode(String.self), + let date = formatter.date(from: dateString) else { + let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Unable to read the date string.") throw DecodingError.dataCorrupted(context) } return Day(date: date) } - public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls _: Bool) throws { + public func encode(into encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls _: Bool) throws { var container = encoder.singleValueContainer() try container.encode(formatter.string(from: date())) } } -/// `Day?` support which mostly just handles `nil` before calling the main ``Day`` codable code. +/// `Day?` support which mostly just handles `nil`. extension Day?: ISO8601Codable { - public static func decode(using decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day? { + public static func decode(from decoder: Decoder, formatter: ISO8601DateFormatter) throws -> Day? { let container = try decoder.singleValueContainer() - return container.decodeNil() ? nil : try Day.decode(using: decoder, formatter: formatter) + return container.decodeNil() ? nil : try Day.decode(from: decoder, formatter: formatter) } - public func encode(using encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws { + public func encode(into encoder: Encoder, formatter: ISO8601DateFormatter, writeNulls: Bool) throws { if let self { - try self.encode(using: encoder, formatter: formatter, writeNulls: writeNulls) + try self.encode(into: encoder, formatter: formatter, writeNulls: writeNulls) } else if writeNulls { var container = encoder.singleValueContainer() try container.encodeNil() diff --git a/Tests/Property wrappers/CustomISO8601Tests.swift b/Tests/Property wrappers/CustomISO8601Tests.swift deleted file mode 100644 index 5ce501a..0000000 --- a/Tests/Property wrappers/CustomISO8601Tests.swift +++ /dev/null @@ -1,105 +0,0 @@ -//import DayType -//import Foundation -//import Testing -// -//private struct ISO8601CustomContainer: Codable where Configurator: CustomISO8601Configurator { -// @CustomISO8601 var d1: Day -// init(d1: Day) { -// self.d1 = d1 -// } -//} -// -//private struct ISO8601CustomOptionalContainer: Codable where Configurator: CustomISO8601Configurator { -// @CustomISO8601 var d1: Day? -// init(d1: Day?) { -// self.d1 = d1 -// } -//} -// -//extension PropertyWrapperSuites { -// -// @Suite("@CustomISO8601") -// struct CustomISO8601Tests { -// -// @Test("Sans Timezone decoding") -// func sansTimeZoneDecoding() throws { -// let json = #"{"d1": "2012-02-02T13:33:23"}"# -// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Local timezone decoding") -// func sansTimeZoneToLocalTimeZoneDecoding() throws { -// enum SansTimeZoneToMelbourneTimeZone: CustomISO8601Configurator { -// static func configure(formatter: ISO8601DateFormatter) { -// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) -// formatter.formatOptions.remove(.withTimeZone) -// } -// } -// let json = #"{"d1": "2012-02-02T13:33:23"}"# -// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 02)) -// } -// -// @Test("Floating to a different timezone decoding") -// func timeZoneOverridingDateTimeZoneDecoding() throws { -// enum BrazilToMelbourneTimeZone: CustomISO8601Configurator { -// static func configure(formatter: ISO8601DateFormatter) { -// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) -// } -// } -// let json = #"{"d1": "2012-02-02T13:33:23-03:00"}"# -// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Numeric iSO8601 decoding") -// func minimalFormatDecoding() throws { -// enum MinimalFormat: CustomISO8601Configurator { -// static func configure(formatter: ISO8601DateFormatter) { -// formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) -// formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) -// formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) -// } -// } -// let json = #"{"d1": "20120202 133323"}"# -// let result = try JSONDecoder().decode(ISO8601CustomContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 02)) -// } -// -// @Test("Optional sans timezone decoding") -// func optionalSansTimeZoneDecoding() throws { -// let json = #"{"d1": "2012-02-02T13:33:23"}"# -// let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Sans timezone decoding of nil") -// func optionalSansTimeZoneWithNilDecoding() throws { -// let json = #"{"d1":null}"# -// let result = try JSONDecoder().decode(ISO8601CustomOptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == nil) -// } -// -// @Test("Custom ISO8601 encoding") -// func encodingISO8601() throws { -// let instance = ISO8601CustomContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) -// } -// -// @Test("Optional Custom ISO8601 encoding") -// func encodingToOptional() throws { -// let instance = ISO8601CustomOptionalContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00"}"#) -// } -// -// @Test("Optional Custom ISO8601 encoding of nil") -// func testEncodingNil() throws { -// let instance = ISO8601CustomOptionalContainer(d1: nil) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) -// } -// } -//} diff --git a/Tests/Property wrappers/ISO8601Tests.swift b/Tests/Property wrappers/ISO8601Tests.swift index 827571d..216b559 100644 --- a/Tests/Property wrappers/ISO8601Tests.swift +++ b/Tests/Property wrappers/ISO8601Tests.swift @@ -1,101 +1,130 @@ -//import DayType -//import Foundation -//import Testing -// -//private struct ISO8601Container: Codable { -// -// @ISO8601 var d1: Day -// init(d1: Day) { -// self.d1 = d1 -// } -//} -// -//private struct ISO8601OptionalContainer: Codable { -// @ISO8601 var d1: Day? -// init(d1: Day?) { -// self.d1 = d1 -// } -//} -// -//extension PropertyWrapperSuites { -// -// @Suite("@ISO8601") -// struct ISO8601Tests { -// -// @Test("Decoding") -// func decoding() throws { -// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# -// let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding a GMT value") -// func decodingWithDefaultGMT() throws { -// let json = #"{"d1": "2012-02-02T13:33:23Z"}"# -// let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding invalid value") -// func decodingWithInvalidStringDate() throws { -// do { -// let json = #"{"d1": "xxxx"}"# -// _ = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.dataCorrupted(let context) { -// #expect(context.debugDescription == "Unable to read a Day value, expected a valid ISO8601 string.") -// #expect(context.codingPath.last?.stringValue == "d1") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Decoding optional") -// func decodingOptional() throws { -// let json = #"{"d1": "2012-02-03T10:33:23+11:00"}"# -// let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == Day(2012, 02, 03)) -// } -// -// @Test("Decoding optional with nil") -// func decodingOptionalWithNil() throws { -// let json = #"{"d1": null}"# -// let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) -// #expect(result.d1 == nil) -// } -// -// @Test("Decoding optional with missing value") -// func decodingOptionalWithMissingValue() throws { -// do { -// let json = #"{}"# -// _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) -// Issue.record("Error not thrown") -// } catch DecodingError.keyNotFound(let key, _) { -// #expect(key.stringValue == "d1") -// } catch { -// Issue.record("Unexpected error: \(error)") -// } -// } -// -// @Test("Encoding") -// func encoding() throws { -// let instance = ISO8601Container(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) -// } -// -// @Test("Encoding optional") -// func encodingOptional() throws { -// let instance = ISO8601OptionalContainer(d1: Day(2012, 02, 03)) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":"2012-02-02T13:00:00Z"}"#) -// } -// -// @Test("Encoding optional with nil") -// func encodingOptionalNil() throws { -// let instance = ISO8601OptionalContainer(d1: nil) -// let result = try JSONEncoder().encode(instance) -// #expect(String(data: result, encoding: .utf8) == #"{"d1":null}"#) -// } -// } -//} +import DayType +import Foundation +import Testing + +private struct ISO8601Container: Codable { + @ISO8601.Default var iso8601: Day + @ISO8601.SansTimezone var iso8601SansTimezone: Day +} + +private struct ISO8601OptionalContainer: Codable { + @ISO8601.Default var iso8601: Day? + @ISO8601.SansTimezone var iso8601SansTimezone: Day? +} + +private struct ISO8601OptionalNullContainer: Codable { + @ISO8601.Default.Nullable var iso8601: Day? + @ISO8601.SansTimezone.Nullable var iso8601SansTimezone: Day? +} + +extension PropertyWrapperSuites { + + @Suite("@ISO8601") + struct ISO8601Tests { + + @Test("Decoding strings") + func decodingDates() throws { + let json = #"{"iso8601": "2012-02-01T12:00:00Z+12:00", "iso8601SansTimezone": "2012-02-01T12:00:00"}"# + let result = try JSONDecoder().decode(ISO8601Container.self, from: json.data(using: .utf8)!) + let expectedDate = Day(2012, 02, 01) + #expect(result.iso8601 == expectedDate) + #expect(result.iso8601SansTimezone == expectedDate) + } + + @Test("Optional string decoding") + func decodingOptionalDates() throws { + let json = #"{"iso8601": "2012-02-01T12:00:00Z+12:00", "iso8601SansTimezone": "2012-02-01T12:00:00"}"# + let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + let expectedDate = Day(2012, 02, 01) + #expect(result.iso8601 == expectedDate) + #expect(result.iso8601SansTimezone == expectedDate) + } + + @Test("Optional nil decoding") + func decodingOptionalNilDates() throws { + let json = #"{"ios8601": null, "iso8691SansTimezone": null}"# + let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.iso8601 == nil) + #expect(result.iso8601SansTimezone == nil) + } + + @Test("Optional missing decoding") + func decodingOptionalMissingDates() throws { + let json = #"{}"# + let result = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + #expect(result.iso8601 == nil) + #expect(result.iso8601SansTimezone == nil) + } + + @Test("Invalid ISO8691 string decoding throws an error") + func decodingInvalidDateThrows() throws { + do { + let json = #"{"iso8601": "xxx4 5 ass3"}"# + _ = try JSONDecoder().decode(ISO8601OptionalContainer.self, from: json.data(using: .utf8)!) + Issue.record("Error not thrown") + } catch DecodingError.dataCorrupted(let context) { + #expect(context.codingPath.map(\.stringValue) == ["iso8601"]) + #expect(context.debugDescription == "Unable to read the date string.") + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test("String encoding") + func encodingDateStrings() throws { + let day = Day(2012, 02, 01) + let instance = ISO8601Container(iso8601: day, iso8601SansTimezone: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + let expectedJSON = #"{"iso8601":"\#(expectedISO8601Date())","iso8601SansTimezone":"\#(expectedISO8601SansDate())"}"# + #expect(json == expectedJSON) + } + + @Test("Optional string encoding") + func encodingOptionalDateStrings() throws { + let day = Day(2012, 02, 01) + let instance = ISO8601OptionalContainer(iso8601: day, iso8601SansTimezone: day) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + let expectedJSON = #"{"iso8601":"\#(expectedISO8601Date())","iso8601SansTimezone":"\#(expectedISO8601SansDate())"}"# + #expect(json == expectedJSON) + } + + @Test("Optional encoding nil") + func encodingOptionalDateStringsWithNil() throws { + let instance = ISO8601OptionalContainer(iso8601: nil, iso8601SansTimezone: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == "{}") + } + + @Test("Optional encoding nil -> null") + func encodingOptionalDateStringsWithNilToNull() throws { + let instance = ISO8601OptionalNullContainer(iso8601: nil, iso8601SansTimezone: nil) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let result = try encoder.encode(instance) + let json = String(data: result, encoding: .utf8)! + #expect(json == #"{"iso8601":null,"iso8601SansTimezone":null}"#) + } + + func expectedISO8601Date() -> String { + let today = DateComponents(calendar: .current, timeZone: .current, year: 2012, month: 2, day: 1) + let formatter = ISO8601DateFormatter() + return formatter.string(from: today.date!) + } + + func expectedISO8601SansDate() -> String { + let today = DateComponents(calendar: .current, timeZone: .current, year: 2012, month: 2, day: 1) + let formatter = ISO8601DateFormatter() + formatter.formatOptions.remove(.withTimeZone) + return formatter.string(from: today.date!) + } + } +} From c116a30f642812d5828c0918a05c5a385c2201c5 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Tue, 13 May 2025 21:59:22 +1000 Subject: [PATCH 26/26] Updating read me. --- README.md | 303 +++++++++++++++++++++--------------------------------- 1 file changed, 117 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index d8df2f3..b89d4a8 100755 --- a/README.md +++ b/README.md @@ -1,274 +1,205 @@ ![Calendar](media/Calendar.png) # DayType -_An API for dates and nothing else. No Calendars, timezones, hours, minutes or seconds. Just dates._ +_An API for dates and nothing else. No Calendars, no timezones, no hours, minutes or seconds. **Just dates!**._ -Swift provides excellent date support through it's `Date`, `Calendar`, `TimeZone` and other types. However there's a catch, they're all designed to work with specific points in time. Not the generalisations that people often use. +Sure swift provides excellent date support with it's `Date`, `Calendar`, `TimeZone` and related types. But there's a catch - they're all designed to work with a specific point in time. And that's not always how people think, and sometimes not even what we get from a server. -For example, you cannot just refer to a person's birthday date without anchoring it to a specific time. In a specific timezone. But people don't consider that and they often don't even know. Just referring to the date in whatever timezone they are in. The same goes for a variety of other dates. Employment leave, religious holidays, retail sales, festivals, etc. All often referred to by date only. +For example we never refer to a person's birthday as being a specific point in time. We don't say "Hey, it's Dave's birthday on the 29th of August at 2:14 am AEST". We simply say the 29th of August and everyone knows what we mean. But Apple's time APIs don't have that generalisation. And that means a whole lot of coding for developers whenever we need to deal in just days'. All sorts of code stripping times, adjusting for time zones, and comparing sanitized dates. All of which is easy to get wrong if you haven't done a lot of it. -As a result developers often find themselves stripping the time and timezone components from Swift's `Date`. Often with mixed results as there are a number of complexities to consider when coercing a point in time to a generalisation. Especially when considering time zones and often questionable input from external sources. - -`DayType` provides simplify date handling through a new type called `Day`. That being a representation of a 24 hours period which is indenpendant of any timezone and not anchored to a specific point in time. ie. there's no hours, minutes, timezones, etc. This allows date code to be simpler because as a developer you no longer needs to sanitise or adjust time components. which alleviates the angst of accidental bugs as well as making the code considerably simpler. +So this is where `DayType` steps it. Basically it provides simplify date handling through a `Day` type which is a representation of a 24 hours period independant of any timezone and without hours, minutes, seconds or milliseconds. In other word a date as people think of it, and that can make your code considerably simpler. ## Installation -`DayType` is a SPM package only. +`DayType` is a SPM package only. So install it as you would install any other package. -# Day +# Introducing Day -The common type you'll use is `Day`. +DayType's common type is `Day` and the DayType package has all sorts of code to read, create and manipulate these `Day` types. Most of which is modelled off Apple's APIs so that as much as possible it will seem familiar. ## Initialisers -`Day` has a number of convenience initialisers which are pretty self explanatory and similar to Swift's `Date` initialisers: +A `Day` has a number of convenience initialisers. Most of which are self explanatory and similar to Swift's `Date`: ```swift -init() -init(daysSince1970: DayInterval) -init(timeIntervalSince1970: TimeInterval) -init(date: Date, usingCalendar calendar: Calendar = .current) -init(components: DayComponents) -init(_ year: Int, _ month: Int, _ day: Int) -init(year: Int, month: Int, day: Int) +init() // Creates a `Day` based on the current time. +init(daysSince1970: DayInterval) // Creates a `Day` using the number of days since 1970. +init(timeIntervalSince1970: TimeInterval) // Creates a `Day` from a `TimeInterval`. +init(date: Date, usingCalendar calendar: Calendar = .current) // Creates a `Day` from a `Date` with an optional calendar. +init(components: DayComponents) // Creates a `Day` from `DayComponents`. +init(_ year: Int, _ month: Int, _ day: Int) // Creates a `Day` from individual year, month and day values. Short form. +init(year: Int, month: Int, day: Int) // Creates a `Day` from individual year, month and day values. ``` ## Properties -### .daysSince1970 - -Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970. - -_Note that matches the number of days produced by this Apple API based code:_ - -```swift -let fromDate = Calendar.current.startOfDay(for: Date(timeIntervalSince1970: 0)) -let toDate = Calendar.current.startOfDay(for: Date()) -let numberOfDays = Calendar.current.dateComponents([.day], from: fromDate, to: toDate).day! -``` +### var daysSince1970: Int { get } -# Property wrappers +Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970. Note this is the number of whole days, dropping any spare seconds. -DayType's property wrappers are designed to address the mostly commonly seen issues when coding and decoding data from external sources. +> Note this effective matches the number of days produced by this API code: +> ```swift +> let fromDate = Calendar.current.startOfDay(for: Date(timeIntervalSince1970: 0)) +> let toDate = Calendar.current.startOfDay(for: Date()) +> let numberOfDays = Calendar.current.dateComponents([.day], from: fromDate, to: toDate).day! +> ``` -_Note: Whilst all of these wrappers support both `Day` and `Day?` properties through the use of the `DayCodable` protocol, it's also technically possible to apply this protocol to other types to make them convertible to a `Day`._ +## Mathematical operators -## @EpochSeconds +`Day` has a number of mathematical operators for adding and subtracting days from a `Day`: -Converts [epoch timestamps](https://www.epochconverter.com) to `Day`. For example the JSON data structure: +```swift -```json -{ - "dob":856616400 -} -``` +// Adding days +let day = Day(2000,1,1) + 5 // -> 2000-01-06 +day += 5 // -> 2000-01-11 -Can be read by: +// Subtracting days +let day = Day(2000,1,1) - 10 // -> 1999-12-21 +day -= 5 // -> 1999-12-16 -```swift -struct MyType: Codable { - @EpochSeconds var dob: Day // or Day? -} +// Obtaining a duration in days +Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration. ``` -## @EpochMilliseconds +## Functions -Essentially the same as `@EpochSeconds` but expects the epoch time to be in millisecond [epoch timestamps](https://www.epochconverter.com). +### func date(inCalendar calendar: Calendar = .current, timeZone: TimeZone? = nil) -> Date -```json -{ - "dob":856616400123 -} -``` +Using the passed `Calendar` and `TimeZone` this function coverts a `Day` to a Swift `Date` with the time components being set to `00:00` (midnight). -Can be read by: +### func day(byAdding component: Day.Component, value: Int) -> Day -```swift -struct MyType: Codable { - @EpochMilliseconds var dob: Day // or Day? -} -``` +Adds any number of years, months or days to a `Day` and returns a new `day`. This is convenient for doing things like producing a sequence of dates for the same day on each month. -## @ISO8601 +### func formatted(_ day: Date.FormatStyle.DateStyle = .abbreviated) -> String -Converts [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date strings to `Day`. +Uses Apple's `Date.formatted(date:time:)` function to format the day into a `String` using the formatting specified in `Date.FormatStyle.DateStyle`. -```json -{ - "dob": "1997-02-22T13:00:00+11:00" -} -``` +# DayComponents -Can be read by: +Similar to how `Date` has `DateComponents` representing the individual parts of a date and time, `Day` has `DayComponents` which contain the day's year, month and day. -```swift -struct MyType: Codable { - @ISO8601 var dob: Day // or Day? -} -``` +# Protocol conformance -## @CustomISO8601 +## Codable -Where `T: DayCodable` and `Configurator: ISO8601Configurator`. +`Day` is fully `Codable`. -Internally `DayType` uses an `ISO8601DateFormatter` to read and write [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) strings. As there are a variety of ISO8601 formats, this property wrapper allows you to pre-configure the formatter before it processes the string. +When encoded or decoded it uses an `Int` representing the number of days since 1 January 1970. This value can also be accessed via the `.daysSince1970` property. -```json -{ - "dob": "20120202 133323" -} -``` +## Equatable -Can be read by: +`Day` is `Equatable` so days can be compared. Ie. ```swift -enum MinimalFormat: ISO8601Configurator { - static func configure(formatter: ISO8601DateFormatter) { - formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60) - formatter.formatOptions.insert(.withSpaceBetweenDateAndTime) - formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) } -} - -struct MyType: Codable { - @CustomISO8601 var dob: Day - // or ... - @CustomISO8601 var dob: Day? -} +Day(2001,2,3) == Day(2001,2,3) // true ``` -The property wrapper is configured trough a `ISO8601Configurator` protocol instance. There's only one function so implementing the protocol is pretty easy. - -_Note that because Swift does not current support specifying a default type for a generic argument, `@CustomISO8601` requires you to specify the `DayCodable` type (`Day` or `Day?`) which must match the type of the property._ - -### Supplied ISO8601 configurators - -#### ISO8601Config.Default - -This configurator does not change the formatter. It's main purpose is to support the `@ISO8601` property wrapper. +## Comparable -#### ISO8601Config.SansTimeZone +`Day` is `Comparable` which enables the comparable operators: `>`, `<`, `>=` and `<=` for comparing days. -This configurator is for the common situation where the ISO8601 string does not have the time zone specified. For example `"1997-02-22T13:00:00"`. +## Hashable -## @DateString +`Day` is also `Hashable` which allows it to be used as dictionary key in in a set. -Where `T: DayCodable` and `Configurator: DateStringConfigurator`. +## Stridable -This property wrapper handles dates stored as strings. It makes use of a custom configurator to specify the format of the date string with a number of common formats supplied. +`Day` is `Stridable` which means you can use it in for loops as well as with the `stride(from:to:by:)` function. For example: -```json -{ - "dob": "2012-12-02" +```swift +for day in Day(2000,1,1)...Day(2000,1,5) { + /// do something with the 1st, 2nd, 3rd, 4th and 5th. } -``` -Can be read by: +for day in Day(2000,1,1).. var dob: Day - // or ... - @DateString var dob: Day? +for day in stride(from: Day(2000,1,1), to: Day(2000,1,5), by: 2) { + /// do something with the 1st and 3rd. } ``` -The `DateStringConfigurator` protocol specifies only a single function which is `static`. That function is used to configure the formatter used to read and write the date strings. - - -_Note: Because Swift does not current support specifying a default type for a generic argument, `@DateString` requires you to specify the `DayCodable` type (`Day` or `Day?`)._ - -### Supplied date string configurators - -#### DateStringConfig.ISO +# Property wrappers -Reads date strings that follow the ISO8601 format but don't have any time components. ie. `2012-12-01' +DayType also provides a number of property wrappers which implement `Codable`, the intent being to allow easy conversions from all sorts of date formats that are often returned from servers. -#### DateStringConfig.DMY +All of the supplied property wrappers can read and write both `Day` and optional `Day?` properties and are grouped by the format of the data they expect to encode and decode. -Reads date strings using the `dd/MM/yyyy` date format. ie. `01/12/2012' +## `@DayString.DMY`, `@DayString.MDY` & `@DayString.YMD` -#### DateStringConfig.MDY - -Reads date strings using the `MM/dd/yyyy` date format. ie. `12/01/2012' - -# Manipulating Day types - -`Day` has also been extended to support a variety of functions and operators. it has `+`, `-`, `+=` and `-=` operators which can be used to add or subtract a number of days from a day. +These property wrappers are designed to encode and decode dates in the `dd/mm/yyyy`, `mm/dd/yyyy` and `yyyy/mm/dd` formats. For example: ```swift -let day = Day(2000,1,1) + 5 // -> 2000-01-06 -let day = Day(2000,1,1) - 10 // -> 1999-12-21 - -let day = Day(2000,1,1) -day += 5 // -> 2000-01-06 - -let day = Day(2000,1,1) -day -= 5 // -> 1999-12-21 +struct MyData { + @DayString.DMY var dmyDay: Day // "31/04/2025" + @DayString.MDY var mdyDay: Day // "04/31/2025" + @DayString.YMD var ymdOptionalDay: Day? // "2025/04/31" +} ``` -And you can subtract one day from another to get the duration between them. +## `@Epoch.seconds` & `@Epoch.milliseconds` + +Encodes and decodes days as [epoch timestamps](https://www.epochconverter.com). For example: ```swift -Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration. +struct MyData { + @Epoch.Seconds var optionalSeconds: Day? // 1746059246 + @Epoch.Milliseconds var milliseconds: Day // 1746059246123 +} ``` -## Functions - -### .date(inCalendar:timeZone:) -> Date - -Using a passed `Calendar` and `TimeZone`, this function coverts a `Day` to a Swift `Date` with the `Day`'s year, month and day, and a time of `00:00` (midnight). With no arguments this function uses the current calendar and time zone. - -### .day(byAdding:, value:) -> Day +## `@ISO8601.Default` and `@ISO8601.SansTimezone` -Lets you add any number of years, months or days to a `Day` and get a new `day` back. This is convenient for doing things like producing a sequence of dates for the same day on each month. - -### .formatted(_:) -> String - -Wrapping `Date.formatted(date:time:)` this function formats a day using the standard formatting specified by the `Date.FormatStyle.DateStyle` styles. The time component of `Date.formatted(date:time:)` is omitted. - -# DayComponents - -Similar to the way `Date` has a matching `DateComponents`, `Day` has a matching `DayComponents`. In this case mostly as a convenient wrapper for passing the individual values for a year, month and day. - -# Protocol conformance +Encodes and decodes standard ISO8061 date strings. The only difference is that `@ISO8601.SansTimezone` is as it's name suggests, intended for reading strings written without a timezone value. For example -## Codable - -`Day` is fully `Codable`. - -It's base value is an `Int` representing the number of days since 1 January 1970 which can accessed via the `.daysSince1970` property. +```swift +struct MyData { + @ISO8601.Default var iso8601: Day // "2025-04-31T12:01:00Z+12:00" + @ISO8601.SansTimezone var OptionalSansTimezone: Day? // "2025-04-31T12:01:00" +} +``` +## Encoding and decoding nulls -## Equatable - -`Day` is `Equatable` so +By default all of DayType's property wrappers can handle decoding where the passed value is a `null` or if there is no value at all. For example: ```swift -Day(2001,2,3) == Day(2001,2,3) // true +struct MyData { + @DayString.DMY var dmy: Day? +} ``` -## Comparable - -`Day` is `Comparable` which lets you use all the comparable operators to compare dates. ie. `>`, `<`, `>=` and `<=`. - -## Hashable +Will read both of these JSONs, setting `dmy` to `nil`: -`Day` is `Hashable` so it can be used as dictionary keys and in sets. +```json +// Null value. +{ + "dmy": null +} -## Stridable +// Missing value. +{} +``` -`Day` is `Stridable` which means you can use it in for loops as well as with the `stride(from:to:by:)` function. +When encoding DayType will skip encoding `nil` values (so producing `{}`), however some API require `null` values. In order to handle these APIs DayType provides some nested property wrappers which will write `null` values instead of skipping the keys all together. For example: ```swift -for day in Day(2000,1,1)...Day(2000,1,5) { - /// do something with the 1st, 2nd, 3rd, 4th and 5th. +struct MyData { + @DayString.DMY.Nullable var dmy: Day? + @Epoch.Seconds.Nullable var seconds: Day? + @ISO8601.Default.Nullable var iso8601: Day? } +``` -for day in Day(2000,1,1)..