Skip to content

Commit acd43c9

Browse files
committed
custom date parser
1 parent 334d865 commit acd43c9

File tree

4 files changed

+224
-31
lines changed

4 files changed

+224
-31
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurren
66

77
let package = Package(
88
name: "swift-aws-lambda-events",
9+
platforms: [.macOS(.v12)],
910
products: [
1011
.library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"])
1112
],

Sources/AWSLambdaEvents/Utils/DateWrappers.swift

Lines changed: 219 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
#if canImport(FoundationEssentials)
16+
import FoundationEssentials
17+
#else
1518
import Foundation
19+
#endif
1620

1721
@propertyWrapper
1822
public struct ISO8601Coding: Decodable, Sendable {
@@ -25,22 +29,16 @@ public struct ISO8601Coding: Decodable, Sendable {
2529
public init(from decoder: Decoder) throws {
2630
let container = try decoder.singleValueContainer()
2731
let dateString = try container.decode(String.self)
28-
guard let date = Self.dateFormatter.date(from: dateString) else {
32+
33+
do {
34+
self.wrappedValue = try Date(dateString, strategy: .iso8601)
35+
} catch {
2936
throw DecodingError.dataCorruptedError(
3037
in: container,
3138
debugDescription:
3239
"Expected date to be in ISO8601 date format, but `\(dateString)` is not in the correct format"
3340
)
3441
}
35-
self.wrappedValue = date
36-
}
37-
38-
private static var dateFormatter: DateFormatter {
39-
let formatter = DateFormatter()
40-
formatter.locale = Locale(identifier: "en_US_POSIX")
41-
formatter.timeZone = TimeZone(secondsFromGMT: 0)
42-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
43-
return formatter
4442
}
4543
}
4644

@@ -55,22 +53,19 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable {
5553
public init(from decoder: Decoder) throws {
5654
let container = try decoder.singleValueContainer()
5755
let dateString = try container.decode(String.self)
58-
guard let date = Self.dateFormatter.date(from: dateString) else {
56+
do {
57+
self.wrappedValue = try Date(dateString, strategy: Self.iso8601WithFractionalSeconds)
58+
} catch {
5959
throw DecodingError.dataCorruptedError(
6060
in: container,
6161
debugDescription:
6262
"Expected date to be in ISO8601 date format with fractional seconds, but `\(dateString)` is not in the correct format"
6363
)
6464
}
65-
self.wrappedValue = date
6665
}
6766

68-
private static var dateFormatter: DateFormatter {
69-
let formatter = DateFormatter()
70-
formatter.locale = Locale(identifier: "en_US_POSIX")
71-
formatter.timeZone = TimeZone(secondsFromGMT: 0)
72-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
73-
return formatter
67+
private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle {
68+
Date.ISO8601FormatStyle(includingFractionalSeconds: true)
7469
}
7570
}
7671

@@ -90,20 +85,18 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable {
9085
if let bracket = string.firstIndex(of: "(") {
9186
string = String(string[string.startIndex..<bracket].trimmingCharacters(in: .whitespaces))
9287
}
93-
for formatter in Self.dateFormatters {
94-
if let date = formatter.date(from: string) {
95-
self.wrappedValue = date
96-
return
97-
}
98-
}
99-
throw DecodingError.dataCorruptedError(
88+
do {
89+
self.wrappedValue = try Date(string, strategy: RFC5322DateStrategy())
90+
} catch {
91+
throw DecodingError.dataCorruptedError(
10092
in: container,
10193
debugDescription:
10294
"Expected date to be in RFC5322 date-time format, but `\(string)` is not in the correct format"
103-
)
95+
)
96+
}
10497
}
10598

106-
private static var dateFormatters: [DateFormatter] {
99+
/*private static var dateFormatters: [DateFormatter] {
107100
// rfc5322 dates received in SES mails sometimes do not include the day, so need two dateformatters
108101
// one with a day and one without
109102
let formatterWithDay = DateFormatter()
@@ -113,5 +106,204 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable {
113106
formatterWithoutDay.dateFormat = "d MMM yyy HH:mm:ss z"
114107
formatterWithoutDay.locale = Locale(identifier: "en_US_POSIX")
115108
return [formatterWithDay, formatterWithoutDay]
109+
}*/
110+
}
111+
112+
struct RFC5322DateError: Error {}
113+
114+
struct RFC5322DateStrategy: ParseStrategy {
115+
func parse(_ input: String) throws -> Date {
116+
guard let components = self.components(from: input) else {
117+
throw RFC5322DateError()
118+
}
119+
guard let date = components.date else {
120+
throw RFC5322DateError()
121+
}
122+
return date
123+
}
124+
125+
func components(from input: String) -> DateComponents? {
126+
var s = input[...]
127+
return s.withUTF8 { buffer -> DateComponents? in
128+
// if the 4th character is a comma, then we have a day of the week
129+
guard buffer.count > 5 else { return nil }
130+
var offset = 0
131+
if buffer[3] == UInt8(ascii: ",") {
132+
offset = 5
133+
}
134+
135+
func parseDay(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
136+
let first = it.next()
137+
let second = it.next()
138+
guard let first = first, let second = second else { return nil }
139+
140+
guard first >= UInt8(ascii: "0") && first <= UInt8(ascii: "9") else { return nil }
141+
142+
if second >= UInt8(ascii: "0") && second <= UInt8(ascii: "9") {
143+
return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0"))
144+
} else {
145+
return Int(first - UInt8(ascii: "0"))
146+
}
147+
}
148+
149+
func skipWhitespace(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> UInt8? {
150+
while let c = it.next() {
151+
if c != UInt8(ascii: " ") {
152+
return c
153+
}
154+
}
155+
return nil
156+
}
157+
158+
func parseMonth(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
159+
let first = it.nextAsciiLetter(skippingWhitespace: true)
160+
let second = it.nextAsciiLetter()
161+
let third = it.nextAsciiLetter()
162+
guard let first = first, let second = second, let third = third else { return nil }
163+
guard first.isAsciiLetter else { return nil}
164+
return monthMap[[first, second, third]]
165+
}
166+
167+
func parseYear(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
168+
let first = it.nextAsciiNumber(skippingWhitespace: true)
169+
let second = it.nextAsciiNumber()
170+
let third = it.nextAsciiNumber()
171+
let fourth = it.nextAsciiNumber()
172+
guard let first = first, let second = second, let third = third, let fourth = fourth else { return nil }
173+
return Int(first - UInt8(ascii: "0")) * 1000 + Int(second - UInt8(ascii: "0")) * 100 + Int(third - UInt8(ascii: "0")) * 10 + Int(fourth - UInt8(ascii: "0"))
174+
}
175+
176+
func parseHour(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
177+
let first = it.nextAsciiNumber(skippingWhitespace: true)
178+
let second = it.nextAsciiNumber()
179+
guard let first = first, let second = second else { return nil }
180+
return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0"))
181+
}
182+
183+
func parseMinute(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
184+
let first = it.nextAsciiNumber(skippingWhitespace: true)
185+
let second = it.nextAsciiNumber()
186+
guard let first = first, let second = second else { return nil }
187+
return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0"))
188+
}
189+
190+
func parseSecond(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
191+
let first = it.nextAsciiNumber(skippingWhitespace: true)
192+
let second = it.nextAsciiNumber()
193+
guard let first = first, let second = second else { return nil }
194+
return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0"))
195+
}
196+
197+
func parseTimezone(_ it: inout UnsafeBufferPointer<UInt8>.Iterator) -> Int? {
198+
let plusMinus = it.nextSkippingWhitespace()
199+
if let plusMinus, plusMinus == UInt8(ascii: "+") || plusMinus == UInt8(ascii: "-") {
200+
let hour = parseHour(&it)
201+
let minute = parseMinute(&it)
202+
guard let hour = hour, let minute = minute else { return nil }
203+
return (hour * 60 + minute) * (plusMinus == UInt8(ascii: "+") ? 1 : -1)
204+
} else if let first = plusMinus {
205+
let second = it.nextAsciiLetter()
206+
let third = it.nextAsciiLetter()
207+
208+
guard let second = second, let third = third else { return nil }
209+
let abbr = [first, second, third]
210+
return timezoneOffsetMap[abbr]
211+
}
212+
213+
return nil
214+
}
215+
216+
var it = buffer.makeIterator()
217+
218+
for _ in 0..<offset {
219+
_ = it.next()
220+
}
221+
222+
guard let day = parseDay(&it) else { return nil }
223+
guard let month = parseMonth(&it) else { return nil }
224+
guard let year = parseYear(&it) else { return nil }
225+
226+
guard let hour = parseHour(&it) else { return nil }
227+
guard it.expect(UInt8(ascii: ":")) else { return nil }
228+
guard let minute = parseMinute(&it) else { return nil }
229+
guard it.expect(UInt8(ascii: ":")) else { return nil }
230+
guard let second = parseSecond(&it) else { return nil }
231+
232+
guard let timezoneOffset = parseTimezone(&it) else { return nil }
233+
234+
return DateComponents(
235+
calendar: Calendar(identifier: .gregorian),
236+
timeZone: TimeZone(secondsFromGMT: timezoneOffset * 60),
237+
year: year,
238+
month: month,
239+
day: day,
240+
hour: hour,
241+
minute: minute,
242+
second: second
243+
)
244+
}
245+
}
246+
}
247+
248+
extension IteratorProtocol where Self.Element == UInt8 {
249+
mutating func expect(_ expected: UInt8) -> Bool {
250+
guard self.next() == expected else { return false }
251+
return true
252+
}
253+
254+
mutating func nextSkippingWhitespace() -> UInt8? {
255+
while let c = self.next() {
256+
if c != UInt8(ascii: " ") {
257+
return c
258+
}
259+
}
260+
return nil
261+
}
262+
263+
mutating func nextAsciiNumber(skippingWhitespace: Bool = false) -> UInt8? {
264+
while let c = self.next() {
265+
if skippingWhitespace {
266+
if c == UInt8(ascii: " ") {
267+
continue
268+
}
269+
}
270+
if c >= UInt8(ascii: "0") && c <= UInt8(ascii: "9") {
271+
return c
272+
} else {
273+
return nil
274+
}
275+
}
276+
return nil
277+
}
278+
279+
mutating func nextAsciiLetter(skippingWhitespace: Bool = false) -> UInt8? {
280+
while let c = self.next() {
281+
if skippingWhitespace {
282+
if c == UInt8(ascii: " ") {
283+
continue
284+
}
285+
}
286+
if c >= UInt8(ascii: "A") && c <= UInt8(ascii: "Z") || c >= UInt8(ascii: "a") && c <= UInt8(ascii: "z") {
287+
return c
288+
} else {
289+
return nil
290+
}
291+
}
292+
return nil
293+
}
294+
}
295+
296+
extension UInt8 {
297+
var isAsciiLetter: Bool {
298+
return self >= UInt8(ascii: "A") && self <= UInt8(ascii: "Z") || self >= UInt8(ascii: "a") && self <= UInt8(ascii: "z")
116299
}
117300
}
301+
302+
let monthMap : [Array<UInt8> : Int ] = [
303+
Array("Jan".utf8): 1, Array("Feb".utf8): 2, Array("Mar".utf8): 3, Array("Apr".utf8): 4, Array("May".utf8): 5, Array("Jun".utf8): 6,
304+
Array("Jul".utf8): 7, Array("Aug".utf8): 8, Array("Sep".utf8): 9, Array("Oct".utf8): 10, Array("Nov".utf8): 11, Array("Dec".utf8): 12
305+
]
306+
307+
let timezoneOffsetMap: [Array<UInt8> : Int] = [
308+
Array("UTC".utf8): 0, Array("GMT".utf8): 0, Array("EDT".utf8): -4 * 60, Array("CDT".utf8): -5 * 60, Array("MDT".utf8): -6 * 60, Array("PDT".utf8): -7 * 60
309+
]

Tests/AWSLambdaEventsTests/SNSTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class SNSTests: XCTestCase {
7272
XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3")
7373
XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5")
7474
XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}")
75-
XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1_578_493_131.203))
75+
XCTAssertEqual(record.sns.timestamp.timeIntervalSince1970, 1_578_493_131.203, accuracy: 0.001)
7676
XCTAssertEqual(record.sns.signatureVersion, "1")
7777
XCTAssertEqual(
7878
record.sns.signature,

Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ class DateWrapperTests: XCTestCase {
4646

4747
XCTAssertEqual(context.codingPath.map(\.stringValue), ["date"])
4848
XCTAssertEqual(
49-
context.debugDescription,
50-
"Expected date to be in ISO8601 date format, but `\(date)` is not in the correct format"
49+
"Expected date to be in ISO8601 date format, but `\(date)` is not in the correct format",
50+
context.debugDescription
5151
)
5252
XCTAssertNil(context.underlyingError)
5353
}
@@ -63,7 +63,7 @@ class DateWrapperTests: XCTestCase {
6363
var event: TestEvent?
6464
XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!))
6565

66-
XCTAssertEqual(event?.date, Date(timeIntervalSince1970: 1_585_241_585.123))
66+
XCTAssertEqual(event?.date.timeIntervalSince1970 ?? 0.0, 1_585_241_585.123, accuracy: 0.001)
6767
}
6868

6969
func testISO8601WithFractionalSecondsCodingWrapperFailure() {

0 commit comments

Comments
 (0)