From 4c6301f6ebf188f003e0850f9544ed50c7c02503 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 02:01:38 -0400 Subject: [PATCH 01/30] Added ability to configure date coding/decoding for UrlEncodedForm --- .../URLEncodedFormDecoder.swift | 28 +++++++++++-- .../URLEncodedFormEncoder.swift | 27 ++++++++++-- Tests/VaporTests/URLEncodedFormTests.swift | 41 +++++++++++++++++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 5fb0df864c..525ca702bd 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -13,9 +13,14 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Used to capture URLForm Coding Configuration used for decoding public struct Configuration { + public enum DateFormat { + case timeIntervalSinceReferenceDate + case timeIntervalSince1970 + case internetDateTime //RFC 3339 of ISO8601 + } let boolFlags: Bool let arraySeparators: [Character] - + let dateFormat: DateFormat /// Creates a new `URLEncodedFormCodingConfiguration`. /// - parameters: /// - boolFlags: Set to `true` allows you to parse `flag1&flag2` as boolean variables @@ -26,10 +31,12 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` public init( boolFlags: Bool = true, - arraySeparators: [Character] = [",", "|"] + arraySeparators: [Character] = [",", "|"], + dateFormat: DateFormat = .internetDateTime ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators + self.dateFormat = dateFormat } } @@ -175,7 +182,22 @@ private struct _Decoder: Decoder { } } else { let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - return try T(from: decoder) + if type == Date.self { + switch configuration.dateFormat { + case .timeIntervalSince1970: + return Date(timeIntervalSince1970: try TimeInterval(from: decoder)) as! T + case .timeIntervalSinceReferenceDate: + return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T + case .internetDateTime: + if let date = ISO8601DateFormatter().date(from: try String(from: decoder)) { + return date as! T + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) + } + } + } else { + return try T(from: decoder) + } } } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 423ddfb753..8dba47f7b6 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -27,15 +27,25 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { case values } + public enum DateFormat { + case timeIntervalSinceReferenceDate + case timeIntervalSince1970 + case internetDateTime //RFC 3339 of ISO8601 + } /// Specified array encoding. public var arrayEncoding: ArrayEncoding + public var dateFormat: DateFormat /// Creates a new `Configuration`. /// /// - parameters: /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. - public init(arrayEncoding: ArrayEncoding = .bracket) { + public init( + arrayEncoding: ArrayEncoding = .bracket, + dateFormat: DateFormat = .internetDateTime + ) { self.arrayEncoding = arrayEncoding + self.dateFormat = dateFormat } } @@ -167,11 +177,22 @@ private class _Encoder: Encoder { func encode(_ value: T, forKey key: Key) throws where T : Encodable { - if let convertible = value as? URLQueryFragmentConvertible { + if let convertible = value as? URLQueryFragmentConvertible { internalData.children[key.stringValue] = URLEncodedFormData(values: [convertible.urlQueryFragmentValue]) } else { let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) - try value.encode(to: encoder) + if let date = value as? Date { + switch configuration.dateFormat { + case .timeIntervalSince1970: + try date.timeIntervalSince1970.encode(to: encoder) + case .timeIntervalSinceReferenceDate: + try date.timeIntervalSinceReferenceDate.encode(to: encoder) + case .internetDateTime: + try ISO8601DateFormatter().string(from: date).encode(to: encoder) + } + } else { + try value.encode(to: encoder) + } self.internalData.children[key.stringValue] = try encoder.getData() } } diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index edc42b09f6..1c55c82a39 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -114,6 +114,43 @@ final class URLEncodedFormTests: XCTestCase { XCTAssert(result.contains("isCool=true")) } + func testDateCoding() throws { + let toEncode = DateCoding(date: Date(timeIntervalSince1970: 0)) + let resultForTimeIntervalSince1970 = try URLEncodedFormEncoder( + configuration: .init(dateFormat: .timeIntervalSince1970) + ).encode(toEncode) + XCTAssertEqual("date=0.0", resultForTimeIntervalSince1970) + + let decodedTimeIntervalSince1970 = try URLEncodedFormDecoder( + configuration: .init(dateFormat: .timeIntervalSince1970) + ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) + XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) + + let resultForTimeIntervalSinceReferenceDate = try URLEncodedFormEncoder( + configuration: .init(dateFormat: .timeIntervalSinceReferenceDate) + ).encode(toEncode) + XCTAssertEqual("date=-978307200.0", resultForTimeIntervalSinceReferenceDate) + + let decodedTimeIntervalSinceReferenceDate = try URLEncodedFormDecoder( + configuration: .init(dateFormat: .timeIntervalSinceReferenceDate) + ).decode(DateCoding.self, from: resultForTimeIntervalSinceReferenceDate) + XCTAssertEqual(decodedTimeIntervalSinceReferenceDate, toEncode) + + let resultForInternetDateTime = try URLEncodedFormEncoder( + configuration: .init(dateFormat: .internetDateTime) + ).encode(toEncode) + XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) + + let decodedInternetDateTime = try URLEncodedFormDecoder( + configuration: .init(dateFormat: .internetDateTime) + ).decode(DateCoding.self, from: resultForInternetDateTime) + XCTAssertEqual(decodedInternetDateTime, toEncode) + + XCTAssertThrowsError(try URLEncodedFormDecoder( + configuration: .init(dateFormat: .internetDateTime) + ).decode(DateCoding.self, from: "date=bad-date")) + } + func testEncodedArrayValues() throws { let user = User(name: "Tanner", age: 23, pets: ["Zizek", "Foo"], dict: ["a": 1, "b": 2], foos: [.baz], nums: [3.14], isCool: true) let result = try URLEncodedFormEncoder( @@ -469,3 +506,7 @@ private struct Users: Codable, Equatable { private enum Foo: String, Codable { case foo, bar, baz } + +struct DateCoding: Codable, Equatable { + let date: Date +} From a318cac5be8c1bf1c79ecbea39b66cec68152da9 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 02:14:03 -0400 Subject: [PATCH 02/30] Added documentation to the `DateFormat` enum --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 6 +++++- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 525ca702bd..a71821d5e8 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -13,10 +13,14 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Used to capture URLForm Coding Configuration used for decoding public struct Configuration { + /// Supported date formats public enum DateFormat { + /// Seconds since 00:00:00 UTC on 1 January 2001 case timeIntervalSinceReferenceDate + /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 - case internetDateTime //RFC 3339 of ISO8601 + /// ISO 8601 formatted date + case internetDateTime } let boolFlags: Bool let arraySeparators: [Character] diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 8dba47f7b6..4e4ac0b159 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -27,10 +27,14 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { case values } + /// Supported date formats public enum DateFormat { + /// Seconds since 00:00:00 UTC on 1 January 2001 case timeIntervalSinceReferenceDate + /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 - case internetDateTime //RFC 3339 of ISO8601 + /// ISO 8601 formatted date + case internetDateTime } /// Specified array encoding. public var arrayEncoding: ArrayEncoding From 569205e287857830eadf28df3bea693be9f6f4cd Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 02:33:33 -0400 Subject: [PATCH 03/30] renamed internetDateTime to iso8601 --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 6 +++--- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 6 +++--- Tests/VaporTests/URLEncodedFormTests.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index a71821d5e8..ba934fe9a8 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -20,7 +20,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date - case internetDateTime + case iso8601 } let boolFlags: Bool let arraySeparators: [Character] @@ -36,7 +36,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormat: DateFormat = .internetDateTime + dateFormat: DateFormat = .iso8601 ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators @@ -192,7 +192,7 @@ private struct _Decoder: Decoder { return Date(timeIntervalSince1970: try TimeInterval(from: decoder)) as! T case .timeIntervalSinceReferenceDate: return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T - case .internetDateTime: + case .iso8601: if let date = ISO8601DateFormatter().date(from: try String(from: decoder)) { return date as! T } else { diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 4e4ac0b159..3e81d0d624 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -34,7 +34,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date - case internetDateTime + case iso8601 } /// Specified array encoding. public var arrayEncoding: ArrayEncoding @@ -46,7 +46,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .internetDateTime + dateFormat: DateFormat = .iso8601 ) { self.arrayEncoding = arrayEncoding self.dateFormat = dateFormat @@ -191,7 +191,7 @@ private class _Encoder: Encoder { try date.timeIntervalSince1970.encode(to: encoder) case .timeIntervalSinceReferenceDate: try date.timeIntervalSinceReferenceDate.encode(to: encoder) - case .internetDateTime: + case .iso8601: try ISO8601DateFormatter().string(from: date).encode(to: encoder) } } else { diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 1c55c82a39..023c49adee 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -137,7 +137,7 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual(decodedTimeIntervalSinceReferenceDate, toEncode) let resultForInternetDateTime = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .internetDateTime) + configuration: .init(dateFormat: .iso8601) ).encode(toEncode) XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) @@ -147,7 +147,7 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual(decodedInternetDateTime, toEncode) XCTAssertThrowsError(try URLEncodedFormDecoder( - configuration: .init(dateFormat: .internetDateTime) + configuration: .init(dateFormat: .iso8601) ).decode(DateCoding.self, from: "date=bad-date")) } From 47d275c430a5f8a9abd6f2a03af58bda9907c2cf Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 02:44:52 -0400 Subject: [PATCH 04/30] Added custom date formatter --- .../URLEncodedForm/URLEncodedFormDecoder.swift | 8 ++++++++ .../URLEncodedForm/URLEncodedFormEncoder.swift | 4 ++++ Tests/VaporTests/URLEncodedFormTests.swift | 17 ++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index ba934fe9a8..baf6212501 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -21,6 +21,8 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { case timeIntervalSince1970 /// ISO 8601 formatted date case iso8601 + /// Use provided `DateFormatter` + case custom(DateFormatter) } let boolFlags: Bool let arraySeparators: [Character] @@ -198,6 +200,12 @@ private struct _Decoder: Decoder { } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) } + case .custom(let dateFormatter): + if let date = dateFormatter.date(from: try String(from: decoder)) { + return date as! T + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Custom date formatter in use")) + } } } else { return try T(from: decoder) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 3e81d0d624..23f34525d0 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -35,6 +35,8 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { case timeIntervalSince1970 /// ISO 8601 formatted date case iso8601 + /// Use provided `DateFormatter` + case custom(DateFormatter) } /// Specified array encoding. public var arrayEncoding: ArrayEncoding @@ -193,6 +195,8 @@ private class _Encoder: Encoder { try date.timeIntervalSinceReferenceDate.encode(to: encoder) case .iso8601: try ISO8601DateFormatter().string(from: date).encode(to: encoder) + case .custom(let dateFormatter): + try dateFormatter.string(from: date).encode(to: encoder) } } else { try value.encode(to: encoder) diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 023c49adee..5fdd2a9f41 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -142,13 +142,28 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) let decodedInternetDateTime = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .internetDateTime) + configuration: .init(dateFormat: .iso8601) ).decode(DateCoding.self, from: resultForInternetDateTime) XCTAssertEqual(decodedInternetDateTime, toEncode) XCTAssertThrowsError(try URLEncodedFormDecoder( configuration: .init(dateFormat: .iso8601) ).decode(DateCoding.self, from: "date=bad-date")) + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "'Date:' yyyy-MM-dd 'Time:' HH:mm:ss 'Timezone:' ZZZZZ" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + let resultCustom = try URLEncodedFormEncoder( + configuration: .init(dateFormat: .custom(dateFormatter)) + ).encode(toEncode) + XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) + + let decodedCustom = try URLEncodedFormDecoder( + configuration: .init(dateFormat: .custom(dateFormatter)) + ).decode(DateCoding.self, from: resultCustom) + XCTAssertEqual(decodedCustom, toEncode) } func testEncodedArrayValues() throws { From ad50caaf24a8c679cced55f50cf75d2351d64415 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 02:47:49 -0400 Subject: [PATCH 05/30] Added comment about `ISO8601DateFormatter` performance --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 1 + Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index baf6212501..86a64ee042 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -195,6 +195,7 @@ private struct _Decoder: Decoder { case .timeIntervalSinceReferenceDate: return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T case .iso8601: + //Creating a new `ISO8601DateFormatter` everytime is probably not performant if let date = ISO8601DateFormatter().date(from: try String(from: decoder)) { return date as! T } else { diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 23f34525d0..441b8f8790 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -194,6 +194,7 @@ private class _Encoder: Encoder { case .timeIntervalSinceReferenceDate: try date.timeIntervalSinceReferenceDate.encode(to: encoder) case .iso8601: + //Creating a new `ISO8601DateFormatter` everytime is probably not performant try ISO8601DateFormatter().string(from: date).encode(to: encoder) case .custom(let dateFormatter): try dateFormatter.string(from: date).encode(to: encoder) From 4f8165f4306689747d5da3f07ed8788f2e158a87 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 10:31:28 -0400 Subject: [PATCH 06/30] Added ISO8601DateFormatter.threadSpecific so a new ISO8601DateFormatter isn't created for every encode/decode. --- .../ISO8601DateFormatter+threadSpecific.swift | 24 +++++++++++++++++++ .../URLEncodedFormDecoder.swift | 2 +- .../URLEncodedFormEncoder.swift | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift diff --git a/Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift b/Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift new file mode 100644 index 0000000000..aecc796bd0 --- /dev/null +++ b/Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift @@ -0,0 +1,24 @@ +// +// File.swift +// +// +// Created by Ravneet Singh on 3/29/20. +// + +import NIO + +fileprivate final class ISO8601 { + fileprivate static let thread: ThreadSpecificVariable = .init() +} + +extension ISO8601DateFormatter { + static var threadSpecific: ISO8601DateFormatter { + if let existing = ISO8601.thread.currentValue { + return existing + } else { + let new = ISO8601DateFormatter() + ISO8601.thread.currentValue = new + return new + } + } +} diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 86a64ee042..ccf07298ea 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -196,7 +196,7 @@ private struct _Decoder: Decoder { return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant - if let date = ISO8601DateFormatter().date(from: try String(from: decoder)) { + if let date = ISO8601DateFormatter.threadSpecific.date(from: try String(from: decoder)) { return date as! T } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 441b8f8790..65b2e7cbf5 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -195,7 +195,7 @@ private class _Encoder: Encoder { try date.timeIntervalSinceReferenceDate.encode(to: encoder) case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant - try ISO8601DateFormatter().string(from: date).encode(to: encoder) + try ISO8601DateFormatter.threadSpecific.string(from: date).encode(to: encoder) case .custom(let dateFormatter): try dateFormatter.string(from: date).encode(to: encoder) } From f2126615ec31bf5cb30cd6c6091b2dfa0238ce31 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 10:34:31 -0400 Subject: [PATCH 07/30] Use typealias instead of declaring DateFormat 2x --- .../Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index ccf07298ea..4c3760e029 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -14,16 +14,8 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Used to capture URLForm Coding Configuration used for decoding public struct Configuration { /// Supported date formats - public enum DateFormat { - /// Seconds since 00:00:00 UTC on 1 January 2001 - case timeIntervalSinceReferenceDate - /// Seconds since 00:00:00 UTC on 1 January 1970 - case timeIntervalSince1970 - /// ISO 8601 formatted date - case iso8601 - /// Use provided `DateFormatter` - case custom(DateFormatter) - } + public typealias DateFormat = URLEncodedFormEncoder.Configuration.DateFormat + let boolFlags: Bool let arraySeparators: [Character] let dateFormat: DateFormat From 78908d116204488e7e6de5a1d9785a3ef11187a0 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 10:55:56 -0400 Subject: [PATCH 08/30] Added `ThreadSpecificDateFormatter` to ensure thread safety when using a `custom` DateFormat for `URLEncodedFormEncoder` or `URLEncodedFormDecoder` --- ...ift => DateFormatter+threadSpecific.swift} | 6 ++++ .../URLEncodedFormDecoder.swift | 4 +-- .../URLEncodedFormEncoder.swift | 12 ++++--- Tests/VaporTests/URLEncodedFormTests.swift | 32 +++++++++++++++---- 4 files changed, 40 insertions(+), 14 deletions(-) rename Sources/Vapor/URLEncodedForm/{ISO8601DateFormatter+threadSpecific.swift => DateFormatter+threadSpecific.swift} (72%) diff --git a/Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift similarity index 72% rename from Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift rename to Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift index aecc796bd0..e6ac2bf825 100644 --- a/Sources/Vapor/URLEncodedForm/ISO8601DateFormatter+threadSpecific.swift +++ b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift @@ -22,3 +22,9 @@ extension ISO8601DateFormatter { } } } + +/// Should return a `DateFormatter` that is thread specific +public protocol ThreadSpecificDateFormatter { + /// Returns a thread specific `DateFormatter` + var currentValue: DateFormatter { get } +} diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 4c3760e029..e2e313580e 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -193,8 +193,8 @@ private struct _Decoder: Decoder { } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) } - case .custom(let dateFormatter): - if let date = dateFormatter.date(from: try String(from: decoder)) { + case .custom(let threadSpecificDateFormatter): + if let date = threadSpecificDateFormatter.currentValue.date(from: try String(from: decoder)) { return date as! T } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Custom date formatter in use")) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 65b2e7cbf5..b0056edc3d 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -1,3 +1,5 @@ +import NIO + /// Encodes `Encodable` instances to `application/x-www-form-urlencoded` data. /// /// print(user) /// User @@ -28,15 +30,15 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// Supported date formats - public enum DateFormat { + public enum DateFormat { /// Seconds since 00:00:00 UTC on 1 January 2001 case timeIntervalSinceReferenceDate /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date case iso8601 - /// Use provided `DateFormatter` - case custom(DateFormatter) + /// Use provided `ThreadSpecificVariable`. Should override `public var currentValue: Value?` to produce a new value + case custom(ThreadSpecificDateFormatter) } /// Specified array encoding. public var arrayEncoding: ArrayEncoding @@ -196,8 +198,8 @@ private class _Encoder: Encoder { case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant try ISO8601DateFormatter.threadSpecific.string(from: date).encode(to: encoder) - case .custom(let dateFormatter): - try dateFormatter.string(from: date).encode(to: encoder) + case .custom(let threadSpecificDateFormatter): + try threadSpecificDateFormatter.currentValue.string(from: date).encode(to: encoder) } } else { try value.encode(to: encoder) diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 5fdd2a9f41..a95be7e152 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -1,5 +1,6 @@ @testable import Vapor import XCTest +import NIO final class URLEncodedFormTests: XCTestCase { // MARK: Codable @@ -149,19 +150,36 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertThrowsError(try URLEncodedFormDecoder( configuration: .init(dateFormat: .iso8601) ).decode(DateCoding.self, from: "date=bad-date")) - - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "'Date:' yyyy-MM-dd 'Time:' HH:mm:ss 'Timezone:' ZZZZZ" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + class DateFormatterFactory: ThreadSpecificDateFormatter { + private var threadSpecificValue = ThreadSpecificVariable() + var currentValue: DateFormatter { + get { + guard let dateFormatter = threadSpecificValue.currentValue else { + let threadSpecificDateFormatter = self.newDateFormatter + threadSpecificValue.currentValue = threadSpecificDateFormatter + return threadSpecificDateFormatter + } + return dateFormatter + } + } + + private var newDateFormatter: DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "'Date:' yyyy-MM-dd 'Time:' HH:mm:ss 'Timezone:' ZZZZZ" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + return dateFormatter + } + } let resultCustom = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .custom(dateFormatter)) + configuration: .init(dateFormat: .custom(DateFormatterFactory())) ).encode(toEncode) XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) let decodedCustom = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .custom(dateFormatter)) + configuration: .init(dateFormat: .custom(DateFormatterFactory())) ).decode(DateCoding.self, from: resultCustom) XCTAssertEqual(decodedCustom, toEncode) } From b9bdedb1a89af1b458c4e81067392dfd2aefb9f3 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 11:09:11 -0400 Subject: [PATCH 09/30] Fixed comment --- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index b0056edc3d..53607e8500 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -30,14 +30,14 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// Supported date formats - public enum DateFormat { + public enum DateFormat { /// Seconds since 00:00:00 UTC on 1 January 2001 case timeIntervalSinceReferenceDate /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date case iso8601 - /// Use provided `ThreadSpecificVariable`. Should override `public var currentValue: Value?` to produce a new value + /// Use provided `ThreadSpecificDateFormatter` case custom(ThreadSpecificDateFormatter) } /// Specified array encoding. From 287dff9cba42475e25efc38206cfac93b3e63a4e Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 18:35:45 -0400 Subject: [PATCH 10/30] Changed custom interface to mimic `JSONDecoder.DateDecodingStrategy.custom(_:)` and `JSONEncoder.DateEncodingStrategy.custom(_:)` interfaces --- .../URLEncodedFormDecoder.swift | 19 ++++++++++++------- .../URLEncodedFormEncoder.swift | 8 ++++---- Tests/VaporTests/URLEncodedFormTests.swift | 16 +++++++++++++--- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index e2e313580e..edbef36de1 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -14,7 +14,16 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Used to capture URLForm Coding Configuration used for decoding public struct Configuration { /// Supported date formats - public typealias DateFormat = URLEncodedFormEncoder.Configuration.DateFormat + public enum DateFormat { + /// Seconds since 00:00:00 UTC on 1 January 2001 + case timeIntervalSinceReferenceDate + /// Seconds since 00:00:00 UTC on 1 January 1970 + case timeIntervalSince1970 + /// ISO 8601 formatted date + case iso8601 + /// Using custom callback + case custom((Decoder) throws -> Date) + } let boolFlags: Bool let arraySeparators: [Character] @@ -193,12 +202,8 @@ private struct _Decoder: Decoder { } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) } - case .custom(let threadSpecificDateFormatter): - if let date = threadSpecificDateFormatter.currentValue.date(from: try String(from: decoder)) { - return date as! T - } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Custom date formatter in use")) - } + case .custom(let callback): + return try callback(decoder) as! T } } else { return try T(from: decoder) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 53607e8500..311b57f627 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -37,8 +37,8 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { case timeIntervalSince1970 /// ISO 8601 formatted date case iso8601 - /// Use provided `ThreadSpecificDateFormatter` - case custom(ThreadSpecificDateFormatter) + /// Using custom callback + case custom((Date, Encoder) throws -> Void) } /// Specified array encoding. public var arrayEncoding: ArrayEncoding @@ -198,8 +198,8 @@ private class _Encoder: Encoder { case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant try ISO8601DateFormatter.threadSpecific.string(from: date).encode(to: encoder) - case .custom(let threadSpecificDateFormatter): - try threadSpecificDateFormatter.currentValue.string(from: date).encode(to: encoder) + case .custom(let callback): + try callback(date, encoder) } } else { try value.encode(to: encoder) diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index a95be7e152..032ab6a9bb 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -172,14 +172,24 @@ final class URLEncodedFormTests: XCTestCase { return dateFormatter } } - + let factory = DateFormatterFactory() let resultCustom = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .custom(DateFormatterFactory())) + configuration: .init(dateFormat: .custom({ (date, encoder) in + var container = encoder.singleValueContainer() + try container.encode(factory.currentValue.string(from: date)) + })) ).encode(toEncode) XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) let decodedCustom = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .custom(DateFormatterFactory())) + configuration: .init(dateFormat: .custom({ (decoder) -> Date in + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let date = factory.currentValue.date(from: string) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to decode date from string '\(string)'") + } + return date + })) ).decode(DateCoding.self, from: resultCustom) XCTAssertEqual(decodedCustom, toEncode) } From bbaf3e8766a49ee9d0928776e4c7d724982ff758 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 29 Mar 2020 18:42:03 -0400 Subject: [PATCH 11/30] Removed unused ThreadSpecificDateFormatter --- .../Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift | 6 ------ Tests/VaporTests/URLEncodedFormTests.swift | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift index e6ac2bf825..aecc796bd0 100644 --- a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift +++ b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift @@ -22,9 +22,3 @@ extension ISO8601DateFormatter { } } } - -/// Should return a `DateFormatter` that is thread specific -public protocol ThreadSpecificDateFormatter { - /// Returns a thread specific `DateFormatter` - var currentValue: DateFormatter { get } -} diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 032ab6a9bb..0156c81177 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -151,7 +151,7 @@ final class URLEncodedFormTests: XCTestCase { configuration: .init(dateFormat: .iso8601) ).decode(DateCoding.self, from: "date=bad-date")) - class DateFormatterFactory: ThreadSpecificDateFormatter { + class DateFormatterFactory { private var threadSpecificValue = ThreadSpecificVariable() var currentValue: DateFormatter { get { From 73617bf5677607474aee9e4be5dd96f8a78edf1c Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 30 Mar 2020 10:17:50 -0400 Subject: [PATCH 12/30] `ISO8601DateFormatter` seems to be thread safe. This bug was filed: https://bugs.swift.org/browse/SR-7745?page=com.atlassian.jira.plugin.system.issuetabpanels%3Aall-tabpanel I tried running the sample program with `10000000` iterations in the `vapor/swift:5.2` docker image without any issue. --- .../DateFormatter+threadSpecific.swift | 14 +++----------- .../URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- .../URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift index aecc796bd0..83161686ac 100644 --- a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift +++ b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift @@ -7,18 +7,10 @@ import NIO -fileprivate final class ISO8601 { - fileprivate static let thread: ThreadSpecificVariable = .init() -} +fileprivate let sharedISO8601DateFormatter = ISO8601DateFormatter() extension ISO8601DateFormatter { - static var threadSpecific: ISO8601DateFormatter { - if let existing = ISO8601.thread.currentValue { - return existing - } else { - let new = ISO8601DateFormatter() - ISO8601.thread.currentValue = new - return new - } + static var shared: ISO8601DateFormatter { + sharedISO8601DateFormatter } } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index edbef36de1..e73b79bdf1 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -197,7 +197,7 @@ private struct _Decoder: Decoder { return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant - if let date = ISO8601DateFormatter.threadSpecific.date(from: try String(from: decoder)) { + if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { return date as! T } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 311b57f627..97dc3632ff 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -197,7 +197,7 @@ private class _Encoder: Encoder { try date.timeIntervalSinceReferenceDate.encode(to: encoder) case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant - try ISO8601DateFormatter.threadSpecific.string(from: date).encode(to: encoder) + try ISO8601DateFormatter.shared.string(from: date).encode(to: encoder) case .custom(let callback): try callback(date, encoder) } From 2cdccb46b159a51dee1df4eb64d54aa543833038 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 30 Mar 2020 10:27:21 -0400 Subject: [PATCH 13/30] Default the date format for URLEncodedFrom coding/decoding to `timeIntervalSinceReferenceDate` --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index e73b79bdf1..6b1c71c480 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -39,7 +39,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormat: DateFormat = .iso8601 + dateFormat: DateFormat = .timeIntervalSinceReferenceDate ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 97dc3632ff..99b5996fd8 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -50,7 +50,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .iso8601 + dateFormat: DateFormat = .timeIntervalSinceReferenceDate ) { self.arrayEncoding = arrayEncoding self.dateFormat = dateFormat From ab98e1e4dc9e4a24919eb66e866d4d62b5073d9f Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 30 Mar 2020 12:57:23 -0400 Subject: [PATCH 14/30] Default to `timeIntervalSince1970` --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 6b1c71c480..27efe59f5b 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -39,7 +39,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormat: DateFormat = .timeIntervalSinceReferenceDate + dateFormat: DateFormat = .timeIntervalSince1970 ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 99b5996fd8..dacf7179a5 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -50,7 +50,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .timeIntervalSinceReferenceDate + dateFormat: DateFormat = .timeIntervalSince1970 ) { self.arrayEncoding = arrayEncoding self.dateFormat = dateFormat From 8b96414afaf44c2b3aee8bfb96792dfb8e57bf91 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 30 Mar 2020 21:23:03 -0400 Subject: [PATCH 15/30] Removed `timeIntervalSinceReferenceDate` option --- .../Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 4 ---- .../Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 4 ---- Tests/VaporTests/URLEncodedFormTests.swift | 10 ---------- 3 files changed, 18 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 27efe59f5b..dc05c1eca2 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -15,8 +15,6 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public struct Configuration { /// Supported date formats public enum DateFormat { - /// Seconds since 00:00:00 UTC on 1 January 2001 - case timeIntervalSinceReferenceDate /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date @@ -193,8 +191,6 @@ private struct _Decoder: Decoder { switch configuration.dateFormat { case .timeIntervalSince1970: return Date(timeIntervalSince1970: try TimeInterval(from: decoder)) as! T - case .timeIntervalSinceReferenceDate: - return Date(timeIntervalSinceReferenceDate: try TimeInterval(from: decoder)) as! T case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index dacf7179a5..37eebf5d7a 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -31,8 +31,6 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Supported date formats public enum DateFormat { - /// Seconds since 00:00:00 UTC on 1 January 2001 - case timeIntervalSinceReferenceDate /// Seconds since 00:00:00 UTC on 1 January 1970 case timeIntervalSince1970 /// ISO 8601 formatted date @@ -193,8 +191,6 @@ private class _Encoder: Encoder { switch configuration.dateFormat { case .timeIntervalSince1970: try date.timeIntervalSince1970.encode(to: encoder) - case .timeIntervalSinceReferenceDate: - try date.timeIntervalSinceReferenceDate.encode(to: encoder) case .iso8601: //Creating a new `ISO8601DateFormatter` everytime is probably not performant try ISO8601DateFormatter.shared.string(from: date).encode(to: encoder) diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 0156c81177..9869f3bee9 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -127,16 +127,6 @@ final class URLEncodedFormTests: XCTestCase { ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) - let resultForTimeIntervalSinceReferenceDate = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .timeIntervalSinceReferenceDate) - ).encode(toEncode) - XCTAssertEqual("date=-978307200.0", resultForTimeIntervalSinceReferenceDate) - - let decodedTimeIntervalSinceReferenceDate = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .timeIntervalSinceReferenceDate) - ).decode(DateCoding.self, from: resultForTimeIntervalSinceReferenceDate) - XCTAssertEqual(decodedTimeIntervalSinceReferenceDate, toEncode) - let resultForInternetDateTime = try URLEncodedFormEncoder( configuration: .init(dateFormat: .iso8601) ).encode(toEncode) From 860d81ac1b3f4447f2ea1d52dd5884cef6d7ea00 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 30 Mar 2020 22:36:46 -0400 Subject: [PATCH 16/30] Made `Date: URLQueryFragmentConvertible` --- .../URLEncodedFormDecoder.swift | 43 +++++++++++-------- .../URLEncodedFormEncoder.swift | 29 +++++++------ .../URLQueryFragmentConvertible.swift | 13 ++++++ 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index dc05c1eca2..11e01faaab 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -170,7 +170,30 @@ private struct _Decoder: Decoder { func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array let child = self.data.children[key.stringValue] ?? [] - if let convertible = T.self as? URLQueryFragmentConvertible.Type { + if T.self is Date.Type { + switch configuration.dateFormat { + case .timeIntervalSince1970: + guard let value = child.values.last else { + throw DecodingError.valueNotFound(T.self, at: self.codingPath + [key]) + } + if let result = Date.init(urlQueryFragmentValue: value) { + return result as! T + } else { + throw DecodingError.typeMismatch(T.self, at: self.codingPath + [key]) + } + case .iso8601: + let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) + //Creating a new `ISO8601DateFormatter` everytime is probably not performant + if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { + return date as! T + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) + } + case .custom(let callback): + let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) + return try callback(decoder) as! T + } + } else if let convertible = T.self as? URLQueryFragmentConvertible.Type { guard let value = child.values.last else { if self.configuration.boolFlags { //If no values found see if we are decoding a boolean @@ -187,23 +210,7 @@ private struct _Decoder: Decoder { } } else { let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - if type == Date.self { - switch configuration.dateFormat { - case .timeIntervalSince1970: - return Date(timeIntervalSince1970: try TimeInterval(from: decoder)) as! T - case .iso8601: - //Creating a new `ISO8601DateFormatter` everytime is probably not performant - if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { - return date as! T - } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) - } - case .custom(let callback): - return try callback(decoder) as! T - } - } else { - return try T(from: decoder) - } + return try T(from: decoder) } } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 37eebf5d7a..0974c90c83 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -183,23 +183,24 @@ private class _Encoder: Encoder { func encode(_ value: T, forKey key: Key) throws where T : Encodable { - if let convertible = value as? URLQueryFragmentConvertible { + if let date = value as? Date { + switch configuration.dateFormat { + case .timeIntervalSince1970: + internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) + case .iso8601: + internalData.children[key.stringValue] = URLEncodedFormData(values: [ + ISO8601DateFormatter.shared.string(from: date).urlQueryFragmentValue + ]) + case .custom(let callback): + let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) + try callback(date, encoder) + self.internalData.children[key.stringValue] = try encoder.getData() + } + } else if let convertible = value as? URLQueryFragmentConvertible { internalData.children[key.stringValue] = URLEncodedFormData(values: [convertible.urlQueryFragmentValue]) } else { let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) - if let date = value as? Date { - switch configuration.dateFormat { - case .timeIntervalSince1970: - try date.timeIntervalSince1970.encode(to: encoder) - case .iso8601: - //Creating a new `ISO8601DateFormatter` everytime is probably not performant - try ISO8601DateFormatter.shared.string(from: date).encode(to: encoder) - case .custom(let callback): - try callback(date, encoder) - } - } else { - try value.encode(to: encoder) - } + try value.encode(to: encoder) self.internalData.children[key.stringValue] = try encoder.getData() } } diff --git a/Sources/Vapor/URLEncodedForm/URLQueryFragmentConvertible.swift b/Sources/Vapor/URLEncodedForm/URLQueryFragmentConvertible.swift index b6d44e2926..ee23b220a2 100644 --- a/Sources/Vapor/URLEncodedForm/URLQueryFragmentConvertible.swift +++ b/Sources/Vapor/URLEncodedForm/URLQueryFragmentConvertible.swift @@ -103,3 +103,16 @@ extension Decimal: URLQueryFragmentConvertible { return .urlDecoded(self.description) } } + +extension Date: URLQueryFragmentConvertible { + init?(urlQueryFragmentValue value: URLQueryFragment) { + guard let double = Double(urlQueryFragmentValue: value) else { + return nil + } + self = Date(timeIntervalSince1970: double) + } + + var urlQueryFragmentValue: URLQueryFragment { + return timeIntervalSince1970.urlQueryFragmentValue + } +} From 70d35a6515bffb498e941f078963561a838bde5c Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Tue, 31 Mar 2020 07:00:44 -0400 Subject: [PATCH 17/30] Date decoding uses an array of DateFormats and tries in order --- .../URLEncodedFormDecoder.swift | 76 ++++++++++++------- .../URLEncodedFormEncoder.swift | 33 ++++---- Tests/VaporTests/URLEncodedFormTests.swift | 22 ++++-- 3 files changed, 83 insertions(+), 48 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 11e01faaab..e7f62a035b 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -16,7 +16,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Supported date formats public enum DateFormat { /// Seconds since 00:00:00 UTC on 1 January 1970 - case timeIntervalSince1970 + case unixTimestamp /// ISO 8601 formatted date case iso8601 /// Using custom callback @@ -25,7 +25,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { let boolFlags: Bool let arraySeparators: [Character] - let dateFormat: DateFormat + let dateFormats: [DateFormat] /// Creates a new `URLEncodedFormCodingConfiguration`. /// - parameters: /// - boolFlags: Set to `true` allows you to parse `flag1&flag2` as boolean variables @@ -34,14 +34,16 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// true, it will always resolve for an optional `Bool`. /// - arraySeparators: Uses these characters to decode arrays. If set to `,`, `arr=v1,v2` would /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` + /// - dateFormat: Date formats used to decode a date. Date formats are tried in the order provided. + /// Defaults to `[.timeIntervalSince1970, .iso8601]` public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormat: DateFormat = .timeIntervalSince1970 + dateFormats: [DateFormat] = [.unixTimestamp, .iso8601] ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators - self.dateFormat = dateFormat + self.dateFormats = dateFormats } } @@ -167,33 +169,51 @@ private struct _Decoder: Decoder { return self.data.children[key.stringValue] == nil } - func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { + private func decodeDate(forKey key: Key) throws -> Date { + for dateFormat in configuration.dateFormats { + do { + return try decodeDate(forKey: key, as: dateFormat) + } catch { + } + } + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) + } + + private func decodeDate(forKey key: Key, as dateFormat: URLEncodedFormDecoder.Configuration.DateFormat) throws -> Date { //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array let child = self.data.children[key.stringValue] ?? [] - if T.self is Date.Type { - switch configuration.dateFormat { - case .timeIntervalSince1970: - guard let value = child.values.last else { - throw DecodingError.valueNotFound(T.self, at: self.codingPath + [key]) - } - if let result = Date.init(urlQueryFragmentValue: value) { - return result as! T - } else { - throw DecodingError.typeMismatch(T.self, at: self.codingPath + [key]) - } - case .iso8601: - let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - //Creating a new `ISO8601DateFormatter` everytime is probably not performant - if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { - return date as! T - } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) - } - case .custom(let callback): - let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - return try callback(decoder) as! T + switch dateFormat { + case .unixTimestamp: + guard let value = child.values.last else { + throw DecodingError.valueNotFound(Date.self, at: self.codingPath + [key]) + } + if let result = Date.init(urlQueryFragmentValue: value) { + return result + } else { + throw DecodingError.typeMismatch(Date.self, at: self.codingPath + [key]) + } + case .iso8601: + let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) + //Creating a new `ISO8601DateFormatter` everytime is probably not performant + if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { + return date + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) } - } else if let convertible = T.self as? URLQueryFragmentConvertible.Type { + case .custom(let callback): + let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) + return try callback(decoder) + } + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { + //Check if we received a date. We need the decode with the appropriate format + guard !(T.self is Date.Type) else { + return try decodeDate(forKey: key) as! T + } + //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array + let child = self.data.children[key.stringValue] ?? [] + if let convertible = T.self as? URLQueryFragmentConvertible.Type { guard let value = child.values.last else { if self.configuration.boolFlags { //If no values found see if we are decoding a boolean diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 0974c90c83..249d84b91e 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -32,7 +32,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Supported date formats public enum DateFormat { /// Seconds since 00:00:00 UTC on 1 January 1970 - case timeIntervalSince1970 + case unixTimestamp /// ISO 8601 formatted date case iso8601 /// Using custom callback @@ -46,9 +46,10 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// /// - parameters: /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. + /// - dateFormat: Format to encode date format too. Defaults to `timeIntervalSince1970` public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .timeIntervalSince1970 + dateFormat: DateFormat = .unixTimestamp ) { self.arrayEncoding = arrayEncoding self.dateFormat = dateFormat @@ -179,23 +180,27 @@ private class _Encoder: Encoder { // skip } + private func encodeDate(_ date: Date, forKey key: Key) throws { + switch configuration.dateFormat { + case .unixTimestamp: + internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) + case .iso8601: + internalData.children[key.stringValue] = URLEncodedFormData(values: [ + ISO8601DateFormatter.shared.string(from: date).urlQueryFragmentValue + ]) + case .custom(let callback): + let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) + try callback(date, encoder) + self.internalData.children[key.stringValue] = try encoder.getData() + } + } + /// See `KeyedEncodingContainerProtocol` func encode(_ value: T, forKey key: Key) throws where T : Encodable { if let date = value as? Date { - switch configuration.dateFormat { - case .timeIntervalSince1970: - internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) - case .iso8601: - internalData.children[key.stringValue] = URLEncodedFormData(values: [ - ISO8601DateFormatter.shared.string(from: date).urlQueryFragmentValue - ]) - case .custom(let callback): - let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) - try callback(date, encoder) - self.internalData.children[key.stringValue] = try encoder.getData() - } + try encodeDate(date, forKey: key) } else if let convertible = value as? URLQueryFragmentConvertible { internalData.children[key.stringValue] = URLEncodedFormData(values: [convertible.urlQueryFragmentValue]) } else { diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 9869f3bee9..e74a7821a2 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -117,13 +117,23 @@ final class URLEncodedFormTests: XCTestCase { func testDateCoding() throws { let toEncode = DateCoding(date: Date(timeIntervalSince1970: 0)) + + let decodedDefaultFromISO8601 = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=1970-01-01T00:00:00Z") + XCTAssertEqual(decodedDefaultFromISO8601, toEncode) + + let resultForDefault = try URLEncodedFormEncoder().encode(toEncode) + XCTAssertEqual("date=0.0", resultForDefault) + + let decodedDefault = try URLEncodedFormDecoder().decode(DateCoding.self, from: resultForDefault) + XCTAssertEqual(decodedDefault, toEncode) + let resultForTimeIntervalSince1970 = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .timeIntervalSince1970) + configuration: .init(dateFormat: .unixTimestamp) ).encode(toEncode) XCTAssertEqual("date=0.0", resultForTimeIntervalSince1970) let decodedTimeIntervalSince1970 = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .timeIntervalSince1970) + configuration: .init(dateFormats: [.unixTimestamp]) ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) @@ -133,12 +143,12 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) let decodedInternetDateTime = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .iso8601) + configuration: .init(dateFormats: [.iso8601]) ).decode(DateCoding.self, from: resultForInternetDateTime) XCTAssertEqual(decodedInternetDateTime, toEncode) XCTAssertThrowsError(try URLEncodedFormDecoder( - configuration: .init(dateFormat: .iso8601) + configuration: .init(dateFormats: [.iso8601]) ).decode(DateCoding.self, from: "date=bad-date")) class DateFormatterFactory { @@ -172,14 +182,14 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) let decodedCustom = try URLEncodedFormDecoder( - configuration: .init(dateFormat: .custom({ (decoder) -> Date in + configuration: .init(dateFormats: [.custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() let string = try container.decode(String.self) guard let date = factory.currentValue.date(from: string) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to decode date from string '\(string)'") } return date - })) + })]) ).decode(DateCoding.self, from: resultCustom) XCTAssertEqual(decodedCustom, toEncode) } From be686167516fff89037d7de49e827315f31b5029 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Tue, 31 Mar 2020 07:02:06 -0400 Subject: [PATCH 18/30] Comments --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index e7f62a035b..b050bafa38 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -34,8 +34,8 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// true, it will always resolve for an optional `Bool`. /// - arraySeparators: Uses these characters to decode arrays. If set to `,`, `arr=v1,v2` would /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` - /// - dateFormat: Date formats used to decode a date. Date formats are tried in the order provided. - /// Defaults to `[.timeIntervalSince1970, .iso8601]` + /// - dateFormats: Date formats used to decode a date. Date formats are tried in the order provided. + /// Defaults to `[.timeIntervalSince1970, .iso8601]` public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], From 76ef4fef47e05fd24be2873f74ec865d7aa19e7a Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Tue, 31 Mar 2020 07:04:13 -0400 Subject: [PATCH 19/30] Removed unnecessary import --- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 249d84b91e..c191285232 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -1,5 +1,3 @@ -import NIO - /// Encodes `Encodable` instances to `application/x-www-form-urlencoded` data. /// /// print(user) /// User From 4750b515cde3793b062e6282c131a9b71be9c508 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Tue, 31 Mar 2020 07:08:47 -0400 Subject: [PATCH 20/30] Comments --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index b050bafa38..813fb15e38 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -15,7 +15,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public struct Configuration { /// Supported date formats public enum DateFormat { - /// Seconds since 00:00:00 UTC on 1 January 1970 + /// Seconds since 1 January 1970 00:00:00 UTC case unixTimestamp /// ISO 8601 formatted date case iso8601 diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index c191285232..4b9166af5d 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -29,7 +29,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Supported date formats public enum DateFormat { - /// Seconds since 00:00:00 UTC on 1 January 1970 + /// Seconds since 1 January 1970 00:00:00 UTC case unixTimestamp /// ISO 8601 formatted date case iso8601 From d5cade46bde40647d648f12943ee9d7db7872ed0 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:27:06 -0400 Subject: [PATCH 21/30] Reintroduced threadSpecific `ISO8601DateFormatter` --- .../DateFormatter+threadSpecific.swift | 21 ++++++++++--------- .../URLEncodedFormDecoder.swift | 2 +- .../URLEncodedFormEncoder.swift | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift index 83161686ac..c29db8201d 100644 --- a/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift +++ b/Sources/Vapor/URLEncodedForm/DateFormatter+threadSpecific.swift @@ -1,16 +1,17 @@ -// -// File.swift -// -// -// Created by Ravneet Singh on 3/29/20. -// - import NIO -fileprivate let sharedISO8601DateFormatter = ISO8601DateFormatter() +fileprivate final class ISO8601 { + fileprivate static let threadSpecific: ThreadSpecificVariable = .init() +} extension ISO8601DateFormatter { - static var shared: ISO8601DateFormatter { - sharedISO8601DateFormatter + static var threadSpecific: ISO8601DateFormatter { + if let existing = ISO8601.threadSpecific.currentValue { + return existing + } else { + let new = ISO8601DateFormatter() + ISO8601.threadSpecific.currentValue = new + return new + } } } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 813fb15e38..1fb09217b5 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -195,7 +195,7 @@ private struct _Decoder: Decoder { case .iso8601: let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) //Creating a new `ISO8601DateFormatter` everytime is probably not performant - if let date = ISO8601DateFormatter.shared.date(from: try String(from: decoder)) { + if let date = ISO8601DateFormatter.threadSpecific.date(from: try String(from: decoder)) { return date } else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date. Expecting ISO8601 formatted date")) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 4b9166af5d..5d68feb2ce 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -184,7 +184,7 @@ private class _Encoder: Encoder { internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) case .iso8601: internalData.children[key.stringValue] = URLEncodedFormData(values: [ - ISO8601DateFormatter.shared.string(from: date).urlQueryFragmentValue + ISO8601DateFormatter.threadSpecific.string(from: date).urlQueryFragmentValue ]) case .custom(let callback): let encoder = _Encoder(codingPath: self.codingPath + [key], configuration: self.configuration) From 0138d6378eaccf1f7c7f978f5ae06a6ffaa7c1ae Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:29:19 -0400 Subject: [PATCH 22/30] Renamed unixTimestamp to secondsSince1970 to match Apple's naming convention --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 6 +++--- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 6 +++--- Tests/VaporTests/URLEncodedFormTests.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 1fb09217b5..0acbb4f01d 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -16,7 +16,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Supported date formats public enum DateFormat { /// Seconds since 1 January 1970 00:00:00 UTC - case unixTimestamp + case secondsSince1970 /// ISO 8601 formatted date case iso8601 /// Using custom callback @@ -39,7 +39,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormats: [DateFormat] = [.unixTimestamp, .iso8601] + dateFormats: [DateFormat] = [.secondsSince1970, .iso8601] ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators @@ -183,7 +183,7 @@ private struct _Decoder: Decoder { //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array let child = self.data.children[key.stringValue] ?? [] switch dateFormat { - case .unixTimestamp: + case .secondsSince1970: guard let value = child.values.last else { throw DecodingError.valueNotFound(Date.self, at: self.codingPath + [key]) } diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 5d68feb2ce..7a66f75d69 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -30,7 +30,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Supported date formats public enum DateFormat { /// Seconds since 1 January 1970 00:00:00 UTC - case unixTimestamp + case secondsSince1970 /// ISO 8601 formatted date case iso8601 /// Using custom callback @@ -47,7 +47,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - dateFormat: Format to encode date format too. Defaults to `timeIntervalSince1970` public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .unixTimestamp + dateFormat: DateFormat = .secondsSince1970 ) { self.arrayEncoding = arrayEncoding self.dateFormat = dateFormat @@ -180,7 +180,7 @@ private class _Encoder: Encoder { private func encodeDate(_ date: Date, forKey key: Key) throws { switch configuration.dateFormat { - case .unixTimestamp: + case .secondsSince1970: internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) case .iso8601: internalData.children[key.stringValue] = URLEncodedFormData(values: [ diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 5afa21cfea..8f3906c44e 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -127,12 +127,12 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual(decodedDefault, toEncode) let resultForTimeIntervalSince1970 = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .unixTimestamp) + configuration: .init(dateFormat: .secondsSince1970) ).encode(toEncode) XCTAssertEqual("date=0.0", resultForTimeIntervalSince1970) let decodedTimeIntervalSince1970 = try URLEncodedFormDecoder( - configuration: .init(dateFormats: [.unixTimestamp]) + configuration: .init(dateFormats: [.secondsSince1970]) ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) From d0fcd9a0a7ccb0137a73138f4d2dd4247db2bbac Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:35:36 -0400 Subject: [PATCH 23/30] Throw last error when decodingDate using multiple decoding approaches --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 0acbb4f01d..1b4610a3e1 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -170,13 +170,15 @@ private struct _Decoder: Decoder { } private func decodeDate(forKey key: Key) throws -> Date { + var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) for dateFormat in configuration.dateFormats { do { return try decodeDate(forKey: key, as: dateFormat) } catch { + lastDecodingError = error } } - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) + throw lastDecodingError } private func decodeDate(forKey key: Key, as dateFormat: URLEncodedFormDecoder.Configuration.DateFormat) throws -> Date { From b62257bc48b5df86b173cc1a1d739b02c39e2d0e Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:39:27 -0400 Subject: [PATCH 24/30] Use `DateDecodingStrategy` and `DateEncodingStrategy` naming convention --- .../Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 12 ++++++------ .../Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 1b4610a3e1..26b65aa4cd 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -14,7 +14,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// Used to capture URLForm Coding Configuration used for decoding public struct Configuration { /// Supported date formats - public enum DateFormat { + public enum DateDecodingStrategy { /// Seconds since 1 January 1970 00:00:00 UTC case secondsSince1970 /// ISO 8601 formatted date @@ -25,7 +25,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { let boolFlags: Bool let arraySeparators: [Character] - let dateFormats: [DateFormat] + let dateDecodingStrategies: [DateDecodingStrategy] /// Creates a new `URLEncodedFormCodingConfiguration`. /// - parameters: /// - boolFlags: Set to `true` allows you to parse `flag1&flag2` as boolean variables @@ -39,11 +39,11 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateFormats: [DateFormat] = [.secondsSince1970, .iso8601] + dateDecodingStrategies: [DateDecodingStrategy] = [.secondsSince1970, .iso8601] ) { self.boolFlags = boolFlags self.arraySeparators = arraySeparators - self.dateFormats = dateFormats + self.dateDecodingStrategies = dateDecodingStrategies } } @@ -171,7 +171,7 @@ private struct _Decoder: Decoder { private func decodeDate(forKey key: Key) throws -> Date { var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) - for dateFormat in configuration.dateFormats { + for dateDecodingStrategy in configuration.dateDecodingStrategies { do { return try decodeDate(forKey: key, as: dateFormat) } catch { @@ -181,7 +181,7 @@ private struct _Decoder: Decoder { throw lastDecodingError } - private func decodeDate(forKey key: Key, as dateFormat: URLEncodedFormDecoder.Configuration.DateFormat) throws -> Date { + private func decodeDate(forKey key: Key, as dateFormat: URLEncodedFormDecoder.Configuration.DateDecodingStrategy) throws -> Date { //If we are trying to decode a required array, we might not have decoded a child, but we should still try to decode an empty array let child = self.data.children[key.stringValue] ?? [] switch dateFormat { diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 7a66f75d69..f555f49e5b 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -28,7 +28,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// Supported date formats - public enum DateFormat { + public enum DateEncodingStrategy { /// Seconds since 1 January 1970 00:00:00 UTC case secondsSince1970 /// ISO 8601 formatted date @@ -38,7 +38,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { } /// Specified array encoding. public var arrayEncoding: ArrayEncoding - public var dateFormat: DateFormat + public var dateEncodingStrategy: DateEncodingStrategy /// Creates a new `Configuration`. /// @@ -47,10 +47,10 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// - dateFormat: Format to encode date format too. Defaults to `timeIntervalSince1970` public init( arrayEncoding: ArrayEncoding = .bracket, - dateFormat: DateFormat = .secondsSince1970 + dateEncodingStrategy: DateEncodingStrategy = .secondsSince1970 ) { self.arrayEncoding = arrayEncoding - self.dateFormat = dateFormat + self.dateEncodingStrategy = dateEncodingStrategy } } @@ -179,7 +179,7 @@ private class _Encoder: Encoder { } private func encodeDate(_ date: Date, forKey key: Key) throws { - switch configuration.dateFormat { + switch configuration.dateEncodingStrategy { case .secondsSince1970: internalData.children[key.stringValue] = URLEncodedFormData(values: [date.urlQueryFragmentValue]) case .iso8601: From 567f6862520740ec6cc4b2d6c6ad5db24297758a Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:43:00 -0400 Subject: [PATCH 25/30] Updated test cases --- .../URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Tests/VaporTests/URLEncodedFormTests.swift | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 26b65aa4cd..adfa774431 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -173,7 +173,7 @@ private struct _Decoder: Decoder { var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) for dateDecodingStrategy in configuration.dateDecodingStrategies { do { - return try decodeDate(forKey: key, as: dateFormat) + return try decodeDate(forKey: key, as: dateDecodingStrategy) } catch { lastDecodingError = error } diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index 8f3906c44e..bcf892d898 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -127,27 +127,27 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual(decodedDefault, toEncode) let resultForTimeIntervalSince1970 = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .secondsSince1970) + configuration: .init(dateEncodingStrategy: .secondsSince1970) ).encode(toEncode) XCTAssertEqual("date=0.0", resultForTimeIntervalSince1970) let decodedTimeIntervalSince1970 = try URLEncodedFormDecoder( - configuration: .init(dateFormats: [.secondsSince1970]) + configuration: .init(dateDecodingStrategies: [.secondsSince1970]) ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) let resultForInternetDateTime = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .iso8601) + configuration: .init(dateEncodingStrategy: .iso8601) ).encode(toEncode) XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) let decodedInternetDateTime = try URLEncodedFormDecoder( - configuration: .init(dateFormats: [.iso8601]) + configuration: .init(dateDecodingStrategies: [.iso8601]) ).decode(DateCoding.self, from: resultForInternetDateTime) XCTAssertEqual(decodedInternetDateTime, toEncode) XCTAssertThrowsError(try URLEncodedFormDecoder( - configuration: .init(dateFormats: [.iso8601]) + configuration: .init(dateDecodingStrategies: [.iso8601]) ).decode(DateCoding.self, from: "date=bad-date")) class DateFormatterFactory { @@ -173,7 +173,7 @@ final class URLEncodedFormTests: XCTestCase { } let factory = DateFormatterFactory() let resultCustom = try URLEncodedFormEncoder( - configuration: .init(dateFormat: .custom({ (date, encoder) in + configuration: .init(dateEncodingStrategy: .custom({ (date, encoder) in var container = encoder.singleValueContainer() try container.encode(factory.currentValue.string(from: date)) })) @@ -181,7 +181,7 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) let decodedCustom = try URLEncodedFormDecoder( - configuration: .init(dateFormats: [.custom({ (decoder) -> Date in + configuration: .init(dateDecodingStrategies: [.custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() let string = try container.decode(String.self) guard let date = factory.currentValue.date(from: string) else { From 2a80df9c7b7189bc228734c8349ebeb28803c814 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Fri, 17 Apr 2020 11:51:20 -0400 Subject: [PATCH 26/30] Updated documentation --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index adfa774431..9b681645d9 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -35,7 +35,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// - arraySeparators: Uses these characters to decode arrays. If set to `,`, `arr=v1,v2` would /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` /// - dateFormats: Date formats used to decode a date. Date formats are tried in the order provided. - /// Defaults to `[.timeIntervalSince1970, .iso8601]` + /// Defaults to `[.secondsSince1970, .iso8601]` public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index f555f49e5b..5724998ccc 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -44,7 +44,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// /// - parameters: /// - arrayEncoding: Specified array encoding. Defaults to `.bracket`. - /// - dateFormat: Format to encode date format too. Defaults to `timeIntervalSince1970` + /// - dateFormat: Format to encode date format too. Defaults to `secondsSince1970` public init( arrayEncoding: ArrayEncoding = .bracket, dateEncodingStrategy: DateEncodingStrategy = .secondsSince1970 From fe655cb7516f8a65ada745063d6ccb3fba548073 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Sun, 19 Apr 2020 16:28:00 -0400 Subject: [PATCH 27/30] Added additional default test and updated documentation --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- Tests/VaporTests/URLEncodedFormTests.swift | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 9b681645d9..3acd8b08f9 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -15,7 +15,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { public struct Configuration { /// Supported date formats public enum DateDecodingStrategy { - /// Seconds since 1 January 1970 00:00:00 UTC + /// Seconds since 1 January 1970 00:00:00 UTC (Unix Timestamp) case secondsSince1970 /// ISO 8601 formatted date case iso8601 diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift index 5724998ccc..339010aad8 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormEncoder.swift @@ -29,7 +29,7 @@ public struct URLEncodedFormEncoder: ContentEncoder, URLQueryEncoder { /// Supported date formats public enum DateEncodingStrategy { - /// Seconds since 1 January 1970 00:00:00 UTC + /// Seconds since 1 January 1970 00:00:00 UTC (Unix Timestamp) case secondsSince1970 /// ISO 8601 formatted date case iso8601 diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index bcf892d898..b017d00683 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -117,6 +117,10 @@ final class URLEncodedFormTests: XCTestCase { func testDateCoding() throws { let toEncode = DateCoding(date: Date(timeIntervalSince1970: 0)) + + let decodedDefaultFromUnixTimestamp = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=0.0") + XCTAssertEqual(decodedDefaultFromUnixTimestamp, toEncode) + let decodedDefaultFromISO8601 = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=1970-01-01T00:00:00Z") XCTAssertEqual(decodedDefaultFromISO8601, toEncode) From 914b8b7902ad9a2f3be9e9df17bb9a0b44a911ee Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 20 Apr 2020 09:57:06 -0400 Subject: [PATCH 28/30] Improved error handling --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 3acd8b08f9..0ebe825de7 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -41,6 +41,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { arraySeparators: [Character] = [",", "|"], dateDecodingStrategies: [DateDecodingStrategy] = [.secondsSince1970, .iso8601] ) { + assert(dateDecodingStrategies.count > 0, "At least one date decoding strategy needs to be defined for `URLEncodedFormDecoder`. Use: ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder(configuration: .init(dateDecodingStrategies: [.secondsSince1970])))") self.boolFlags = boolFlags self.arraySeparators = arraySeparators self.dateDecodingStrategies = dateDecodingStrategies @@ -170,7 +171,8 @@ private struct _Decoder: Decoder { } private func decodeDate(forKey key: Key) throws -> Date { - var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Unable to decode date using the provided date formats")) + //This error will only be thrown if no `dateDecodingStrategies` have been defined. It will get overwritten by the decoders' error if it's thrown + var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "At least one date decoding strategy needs to be defined for `URLEncodedFormDecoder`. Use: ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder(configuration: .init(dateDecodingStrategies: [.secondsSince1970])))")) for dateDecodingStrategy in configuration.dateDecodingStrategies { do { return try decodeDate(forKey: key, as: dateDecodingStrategy) From 0c173b3c57f1c776a3dc07e904bd47c5ba596922 Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Mon, 20 Apr 2020 10:24:24 -0400 Subject: [PATCH 29/30] Removed unnecessary comment --- Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 0ebe825de7..489b61898d 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -198,7 +198,6 @@ private struct _Decoder: Decoder { } case .iso8601: let decoder = _Decoder(data: child, codingPath: self.codingPath + [key], configuration: configuration) - //Creating a new `ISO8601DateFormatter` everytime is probably not performant if let date = ISO8601DateFormatter.threadSpecific.date(from: try String(from: decoder)) { return date } else { From bca712a165ccdaaa472706a49931e24c82ac52ad Mon Sep 17 00:00:00 2001 From: Ravneet Singh Date: Tue, 21 Apr 2020 13:45:33 -0400 Subject: [PATCH 30/30] Replaced dateDecodingStrategies with dateDecodingStrategy --- .../URLEncodedFormDecoder.swift | 21 +++++-------------- Tests/VaporTests/URLEncodedFormTests.swift | 15 ++++++------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift index 489b61898d..197eb8f505 100644 --- a/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Vapor/URLEncodedForm/URLEncodedFormDecoder.swift @@ -25,7 +25,7 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { let boolFlags: Bool let arraySeparators: [Character] - let dateDecodingStrategies: [DateDecodingStrategy] + let dateDecodingStrategy: DateDecodingStrategy /// Creates a new `URLEncodedFormCodingConfiguration`. /// - parameters: /// - boolFlags: Set to `true` allows you to parse `flag1&flag2` as boolean variables @@ -34,17 +34,15 @@ public struct URLEncodedFormDecoder: ContentDecoder, URLQueryDecoder { /// true, it will always resolve for an optional `Bool`. /// - arraySeparators: Uses these characters to decode arrays. If set to `,`, `arr=v1,v2` would /// populate a key named `arr` of type `Array` to be decoded as `["v1", "v2"]` - /// - dateFormats: Date formats used to decode a date. Date formats are tried in the order provided. - /// Defaults to `[.secondsSince1970, .iso8601]` + /// - dateDecodingStrategy: Date format used to decode a date. Date formats are tried in the order provided public init( boolFlags: Bool = true, arraySeparators: [Character] = [",", "|"], - dateDecodingStrategies: [DateDecodingStrategy] = [.secondsSince1970, .iso8601] + dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970 ) { - assert(dateDecodingStrategies.count > 0, "At least one date decoding strategy needs to be defined for `URLEncodedFormDecoder`. Use: ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder(configuration: .init(dateDecodingStrategies: [.secondsSince1970])))") self.boolFlags = boolFlags self.arraySeparators = arraySeparators - self.dateDecodingStrategies = dateDecodingStrategies + self.dateDecodingStrategy = dateDecodingStrategy } } @@ -171,16 +169,7 @@ private struct _Decoder: Decoder { } private func decodeDate(forKey key: Key) throws -> Date { - //This error will only be thrown if no `dateDecodingStrategies` have been defined. It will get overwritten by the decoders' error if it's thrown - var lastDecodingError: Error = DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "At least one date decoding strategy needs to be defined for `URLEncodedFormDecoder`. Use: ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder(configuration: .init(dateDecodingStrategies: [.secondsSince1970])))")) - for dateDecodingStrategy in configuration.dateDecodingStrategies { - do { - return try decodeDate(forKey: key, as: dateDecodingStrategy) - } catch { - lastDecodingError = error - } - } - throw lastDecodingError + return try decodeDate(forKey: key, as: configuration.dateDecodingStrategy) } private func decodeDate(forKey key: Key, as dateFormat: URLEncodedFormDecoder.Configuration.DateDecodingStrategy) throws -> Date { diff --git a/Tests/VaporTests/URLEncodedFormTests.swift b/Tests/VaporTests/URLEncodedFormTests.swift index b017d00683..8abcade36a 100644 --- a/Tests/VaporTests/URLEncodedFormTests.swift +++ b/Tests/VaporTests/URLEncodedFormTests.swift @@ -118,12 +118,9 @@ final class URLEncodedFormTests: XCTestCase { func testDateCoding() throws { let toEncode = DateCoding(date: Date(timeIntervalSince1970: 0)) - let decodedDefaultFromUnixTimestamp = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=0.0") + let decodedDefaultFromUnixTimestamp = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=0") XCTAssertEqual(decodedDefaultFromUnixTimestamp, toEncode) - let decodedDefaultFromISO8601 = try URLEncodedFormDecoder().decode(DateCoding.self, from: "date=1970-01-01T00:00:00Z") - XCTAssertEqual(decodedDefaultFromISO8601, toEncode) - let resultForDefault = try URLEncodedFormEncoder().encode(toEncode) XCTAssertEqual("date=0.0", resultForDefault) @@ -136,7 +133,7 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=0.0", resultForTimeIntervalSince1970) let decodedTimeIntervalSince1970 = try URLEncodedFormDecoder( - configuration: .init(dateDecodingStrategies: [.secondsSince1970]) + configuration: .init(dateDecodingStrategy: .secondsSince1970) ).decode(DateCoding.self, from: resultForTimeIntervalSince1970) XCTAssertEqual(decodedTimeIntervalSince1970, toEncode) @@ -146,12 +143,12 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=1970-01-01T00:00:00Z", resultForInternetDateTime) let decodedInternetDateTime = try URLEncodedFormDecoder( - configuration: .init(dateDecodingStrategies: [.iso8601]) + configuration: .init(dateDecodingStrategy: .iso8601) ).decode(DateCoding.self, from: resultForInternetDateTime) XCTAssertEqual(decodedInternetDateTime, toEncode) XCTAssertThrowsError(try URLEncodedFormDecoder( - configuration: .init(dateDecodingStrategies: [.iso8601]) + configuration: .init(dateDecodingStrategy: .iso8601) ).decode(DateCoding.self, from: "date=bad-date")) class DateFormatterFactory { @@ -185,14 +182,14 @@ final class URLEncodedFormTests: XCTestCase { XCTAssertEqual("date=Date:%201970-01-01%20Time:%2000:00:00%20Timezone:%20Z", resultCustom) let decodedCustom = try URLEncodedFormDecoder( - configuration: .init(dateDecodingStrategies: [.custom({ (decoder) -> Date in + configuration: .init(dateDecodingStrategy: .custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() let string = try container.decode(String.self) guard let date = factory.currentValue.date(from: string) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to decode date from string '\(string)'") } return date - })]) + })) ).decode(DateCoding.self, from: resultCustom) XCTAssertEqual(decodedCustom, toEncode) }