Skip to content

Commit 5907061

Browse files
authored
Fix/thread safety sendable (#146)
- Replace DateFormatter with modern Date.ParseStrategy for thread-safe RFC 2822 parsing - Add Sendable conformance to FileManagerHandler per coding guidelines - Fix async test in DataSourceTests to properly await Task execution - Add comprehensive test coverage for invalid RFC 2822 date formats Resolves #134, #135, #136, #137 🤖 Generated with [Claude Code](https://claude.ai/code)
1 parent 1b3e8d8 commit 5907061

File tree

4 files changed

+47
-15
lines changed

4 files changed

+47
-15
lines changed

Sources/BushelUtilities/Extensions/Date+RFC2822.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,32 @@
3030
public import Foundation
3131

3232
extension Date {
33-
/// RFC 2822 date formatter for HTTP Last-Modified headers
33+
/// RFC 2822 parse strategy for HTTP Last-Modified headers
3434
///
35-
/// Shared static formatter for thread-safe, efficient date parsing.
35+
/// Thread-safe parse strategy using modern Swift FormatStyle API.
3636
/// Format: "EEE, dd MMM yyyy HH:mm:ss zzz"
37-
private static let rfc2822Formatter: DateFormatter = {
38-
let formatter = DateFormatter()
39-
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
40-
formatter.locale = Locale(identifier: "en_US_POSIX")
41-
formatter.timeZone = TimeZone(secondsFromGMT: 0)
42-
return formatter
43-
}()
37+
private static let rfc2822ParseStrategy = Date.ParseStrategy(
38+
format:
39+
// swiftlint:disable:next line_length
40+
"\(weekday: .abbreviated), \(day: .twoDigits) \(month: .abbreviated) \(year: .padded(4)) \(hour: .twoDigits(clock: .twentyFourHour, hourCycle: .zeroBased)):\(minute: .twoDigits):\(second: .twoDigits) GMT",
41+
locale: Locale(identifier: "en_US_POSIX"),
42+
timeZone: TimeZone(secondsFromGMT: 0) ?? .gmt
43+
)
4444

4545
/// Creates a date from an RFC 2822 formatted string
4646
///
4747
/// This initializer is designed for HTTP Last-Modified headers and similar RFC 2822 date formats.
48+
/// Uses modern thread-safe ParseStrategy API for parsing.
4849
///
4950
/// Example: "Fri, 19 Dec 2025 10:30:45 GMT"
5051
///
5152
/// - Parameter rfc2822String: The RFC 2822 formatted date string
5253
/// - Returns: A date if parsing succeeds, nil otherwise
5354
public init?(rfc2822String: String) {
54-
guard let date = Self.rfc2822Formatter.date(from: rfc2822String) else {
55+
do {
56+
self = try Self.rfc2822ParseStrategy.parse(rfc2822String)
57+
} catch {
5558
return nil
5659
}
57-
self = date
5860
}
5961
}

Sources/BushelUtilities/FileManager/FileManagerHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public import Foundation
3434
#endif
3535

3636
/// A struct that provides a wrapper around FileManager for file-related operations.
37-
public struct FileManagerHandler: FileHandler {
37+
public struct FileManagerHandler: FileHandler, Sendable {
3838
/// A closure that provides a `FileManager` instance.
3939
private let fileManager: @Sendable () -> FileManager
4040

Tests/BushelFoundationTests/DataSourceTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ internal final class DataSourceTests: XCTestCase {
8282
}
8383
}
8484

85-
internal func testSendable() {
85+
internal func testSendable() async {
8686
// Compile-time check that DataSource is Sendable
87-
Task {
87+
await Task {
8888
let source = DataSource.appleDB
8989
XCTAssertEqual(source.rawValue, "appledb.dev")
90-
}
90+
}.value
9191
}
9292
}

Tests/BushelFoundationTests/FormattersTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ internal final class FormattersTests: XCTestCase {
128128
)
129129
}
130130

131+
internal func testLastModifiedDateFormatterInvalidInputs() {
132+
let sut = Formatters.lastModifiedDateFormatter
133+
134+
let invalidInputs = [
135+
"", // Empty string
136+
"Not a date", // Completely invalid
137+
"32 Dec 2025 10:30:45 GMT", // Invalid day
138+
"Fri, 19 Foo 2025 10:30:45 GMT", // Invalid month
139+
"Fri, 19 Dec 2025 25:00:00 GMT", // Invalid hour
140+
"19 Dec 2025 10:30:45 GMT", // Missing day name
141+
"Fri, 19 Dec 2025 10:30:45", // Missing timezone
142+
"Fri, 00 Dec 2025 10:30:45 GMT", // Invalid day (0)
143+
"Fri, 19 Dec 2025 24:30:45 GMT", // Invalid hour (24)
144+
"Fri, 19 Dec 2025 10:60:45 GMT", // Invalid minute
145+
"Fri, 19 Dec 2025 10:30:60 GMT", // Invalid second
146+
"Fri, 19 Dec abcd 10:30:45 GMT", // Invalid year
147+
"Xyz, 19 Dec 2025 10:30:45 GMT", // Invalid day name
148+
"Fri, 19 Dec 2025 xx:30:45 GMT", // Invalid hour format
149+
"Fri, 19 Dec 2025 10:xx:45 GMT", // Invalid minute format
150+
"Fri, 19 Dec 2025 10:30:xx GMT", // Invalid second format
151+
]
152+
153+
for input in invalidInputs {
154+
XCTAssertNil(
155+
sut.date(from: input),
156+
"Should return nil for invalid input: \(input)"
157+
)
158+
}
159+
}
160+
131161
// MARK: - FormatStyle Tests
132162

133163
internal func testFormatDate() {

0 commit comments

Comments
 (0)