提交 a516974f 编写于 作者: A Almaz Ibragimov 提交者: Jon Shier

Date encoding strategies for URLEncodedFormEncoder (#2813)

* Added date encoding strategies to `URLEncodedFormEncoder`

* Documentation fixes and error handling in `URLEncodedFormEncoder.DateEncoding`
上级 1081318d
......@@ -197,6 +197,9 @@ open class URLEncodedFormParameterEncoder: ParameterEncoder {
/// `BoolEncoding` can be used to configure how `Bool` values are encoded. The default behavior is to encode
/// `true` as 1 and `false` as 0.
///
/// `DateEncoding` can be used to configure how `Date` values are encoded. By default, the `.deferredToDate`
/// strategy is used, which formats dates from their structure.
///
/// `SpaceEncoding` can be used to configure how spaces are encoded. Modern encodings use percent replacement (%20),
/// while older encoding may expect spaces to be replaced with +.
///
......@@ -221,6 +224,49 @@ public final class URLEncodedFormEncoder {
}
}
/// Configures how `Date` parameters are encoded.
public enum DateEncoding {
/// Defers encoding to the `Date` type.
case deferredToDate
/// Encodes dates as seconds since midnight UTC on January 1, 1970.
case secondsSince1970
/// Encodes dates as milliseconds since midnight UTC on January 1, 1970.
case millisecondsSince1970
/// Encodes dates according to the ISO 8601 and RFC3339 standards.
case iso8601
/// Encodes dates using the given `DateFormatter`.
case formatted(DateFormatter)
/// Encodes dates using the given closure.
case custom((Date) throws -> String)
private static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()
/// Encodes the date according to the strategy.
///
/// - Parameter string: The `Date` to encode.
/// - Returns: The encoded `String` or `nil` if the date should be encoded as `Encodable` structure.
func encode(_ value: Date) throws -> String? {
switch self {
case .deferredToDate:
return nil
case .secondsSince1970:
return String(value.timeIntervalSince1970)
case .millisecondsSince1970:
return String(value.timeIntervalSince1970 * 1000.0)
case .iso8601:
return DateEncoding.iso8601Formatter.string(from: value)
case let .formatted(formatter):
return formatter.string(from: value)
case let .custom(closure):
return try closure(value)
}
}
}
/// Configures how `Array` parameters are encoded.
public enum ArrayEncoding {
/// An empty set of square brackets ("[]") are sppended to the key for every value.
......@@ -271,6 +317,8 @@ public final class URLEncodedFormEncoder {
public let arrayEncoding: ArrayEncoding
/// The `BoolEncoding` to use.
public let boolEncoding: BoolEncoding
/// The `DateEncoding` to use.
public let dateEncoding: DateEncoding
/// The `SpaceEncoding` to use.
public let spaceEncoding: SpaceEncoding
/// The `CharacterSet` of allowed characters.
......@@ -281,21 +329,26 @@ public final class URLEncodedFormEncoder {
/// - Parameters:
/// - arrayEncoding: The `ArrayEncoding` instance. Defaults to `.brackets`.
/// - boolEncoding: The `BoolEncoding` instance. Defaults to `.numeric`.
/// - dateEncoding: The `DateEncoding` instance. Defaults to `.deferredToDate`.
/// - spaceEncoding: The `SpaceEncoding` instance. Defaults to `.percentEscaped`.
/// - allowedCharacters: The `CharacterSet` of allowed (non-escaped) characters. Defaults to `.afURLQueryAllowed`.
public init(arrayEncoding: ArrayEncoding = .brackets,
boolEncoding: BoolEncoding = .numeric,
dateEncoding: DateEncoding = .deferredToDate,
spaceEncoding: SpaceEncoding = .percentEscaped,
allowedCharacters: CharacterSet = .afURLQueryAllowed) {
self.arrayEncoding = arrayEncoding
self.boolEncoding = boolEncoding
self.dateEncoding = dateEncoding
self.spaceEncoding = spaceEncoding
self.allowedCharacters = allowedCharacters
}
func encode(_ value: Encodable) throws -> URLEncodedFormComponent {
let context = URLEncodedFormContext(.object([:]))
let encoder = _URLEncodedFormEncoder(context: context, boolEncoding: boolEncoding)
let encoder = _URLEncodedFormEncoder(context: context,
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
try value.encode(to: encoder)
return context.component
......@@ -342,13 +395,16 @@ final class _URLEncodedFormEncoder {
let context: URLEncodedFormContext
private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
private let dateEncoding: URLEncodedFormEncoder.DateEncoding
public init(context: URLEncodedFormContext,
codingPath: [CodingKey] = [],
boolEncoding: URLEncodedFormEncoder.BoolEncoding) {
boolEncoding: URLEncodedFormEncoder.BoolEncoding,
dateEncoding: URLEncodedFormEncoder.DateEncoding) {
self.context = context
self.codingPath = codingPath
self.boolEncoding = boolEncoding
self.dateEncoding = dateEncoding
}
}
......@@ -356,20 +412,23 @@ extension _URLEncodedFormEncoder: Encoder {
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
let container = _URLEncodedFormEncoder.KeyedContainer<Key>(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
return _URLEncodedFormEncoder.UnkeyedContainer(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
func singleValueContainer() -> SingleValueEncodingContainer {
return _URLEncodedFormEncoder.SingleValueContainer(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
}
......@@ -492,13 +551,16 @@ extension _URLEncodedFormEncoder {
private let context: URLEncodedFormContext
private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
private let dateEncoding: URLEncodedFormEncoder.DateEncoding
init(context: URLEncodedFormContext,
codingPath: [CodingKey],
boolEncoding: URLEncodedFormEncoder.BoolEncoding) {
boolEncoding: URLEncodedFormEncoder.BoolEncoding,
dateEncoding: URLEncodedFormEncoder.DateEncoding) {
self.context = context
self.codingPath = codingPath
self.boolEncoding = boolEncoding
self.dateEncoding = dateEncoding
}
private func nestedCodingPath(for key: CodingKey) -> [CodingKey] {
......@@ -522,7 +584,8 @@ extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol
func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer {
let container = _URLEncodedFormEncoder.SingleValueContainer(context: context,
codingPath: nestedCodingPath(for: key),
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
return container
}
......@@ -530,7 +593,8 @@ extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context,
codingPath: nestedCodingPath(for: key),
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
return container
}
......@@ -538,17 +602,24 @@ extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
let container = _URLEncodedFormEncoder.KeyedContainer<NestedKey>(context: context,
codingPath: nestedCodingPath(for: key),
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
return KeyedEncodingContainer(container)
}
func superEncoder() -> Encoder {
return _URLEncodedFormEncoder(context: context, codingPath: codingPath, boolEncoding: boolEncoding)
return _URLEncodedFormEncoder(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
func superEncoder(forKey key: Key) -> Encoder {
return _URLEncodedFormEncoder(context: context, codingPath: nestedCodingPath(for: key), boolEncoding: boolEncoding)
return _URLEncodedFormEncoder(context: context,
codingPath: nestedCodingPath(for: key),
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
}
......@@ -560,11 +631,16 @@ extension _URLEncodedFormEncoder {
private let context: URLEncodedFormContext
private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
private let dateEncoding: URLEncodedFormEncoder.DateEncoding
init(context: URLEncodedFormContext, codingPath: [CodingKey], boolEncoding: URLEncodedFormEncoder.BoolEncoding) {
init(context: URLEncodedFormContext,
codingPath: [CodingKey],
boolEncoding: URLEncodedFormEncoder.BoolEncoding,
dateEncoding: URLEncodedFormEncoder.DateEncoding) {
self.context = context
self.codingPath = codingPath
self.boolEncoding = boolEncoding
self.dateEncoding = dateEncoding
}
private func checkCanEncode(value: Any?) throws {
......@@ -651,13 +727,24 @@ extension _URLEncodedFormEncoder.SingleValueContainer: SingleValueEncodingContai
}
func encode<T>(_ value: T) throws where T : Encodable {
try checkCanEncode(value: value)
defer { canEncodeNewValue = false }
switch value {
case let date as Date:
guard let string = try dateEncoding.encode(date) else {
fallthrough
}
let encoder = _URLEncodedFormEncoder(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding)
try value.encode(to: encoder)
try encode(value, as: string)
default:
try checkCanEncode(value: value)
defer { canEncodeNewValue = false }
let encoder = _URLEncodedFormEncoder(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
try value.encode(to: encoder)
}
}
}
......@@ -672,13 +759,16 @@ extension _URLEncodedFormEncoder {
private let context: URLEncodedFormContext
private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
private let dateEncoding: URLEncodedFormEncoder.DateEncoding
init(context: URLEncodedFormContext,
codingPath: [CodingKey],
boolEncoding: URLEncodedFormEncoder.BoolEncoding) {
boolEncoding: URLEncodedFormEncoder.BoolEncoding,
dateEncoding: URLEncodedFormEncoder.DateEncoding) {
self.context = context
self.codingPath = codingPath
self.boolEncoding = boolEncoding
self.dateEncoding = dateEncoding
}
}
}
......@@ -700,14 +790,16 @@ extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
return _URLEncodedFormEncoder.SingleValueContainer(context: context,
codingPath: nestedCodingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
defer { count += 1 }
let container = _URLEncodedFormEncoder.KeyedContainer<NestedKey>(context: context,
codingPath: nestedCodingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
return KeyedEncodingContainer(container)
}
......@@ -717,13 +809,17 @@ extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
return _URLEncodedFormEncoder.UnkeyedContainer(context: context,
codingPath: nestedCodingPath,
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
func superEncoder() -> Encoder {
defer { count += 1 }
return _URLEncodedFormEncoder(context: context, codingPath: codingPath, boolEncoding: boolEncoding)
return _URLEncodedFormEncoder(context: context,
codingPath: codingPath,
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)
}
}
......
......@@ -485,6 +485,97 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
XCTAssertEqual(result.value, "bool=true")
}
func testThatDatesCanBeEncoded() {
// Given
let encoder = URLEncodedFormEncoder(dateEncoding: .deferredToDate)
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=123.456")
}
func testThatDatesCanBeEncodedAsSecondsSince1970() {
// Given
let encoder = URLEncodedFormEncoder(dateEncoding: .secondsSince1970)
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=978307323.456")
}
func testThatDatesCanBeEncodedAsMillisecondsSince1970() {
// Given
let encoder = URLEncodedFormEncoder(dateEncoding: .millisecondsSince1970)
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=978307323456.0")
}
func testThatDatesCanBeEncodedAsISO8601Formatted() {
// Given
let encoder = URLEncodedFormEncoder(dateEncoding: .iso8601)
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=2001-01-01T00%3A02%3A03Z")
}
func testThatDatesCanBeEncodedAsFormatted() {
// Given
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
let encoder = URLEncodedFormEncoder(dateEncoding: .formatted(dateFormatter))
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=2001-01-01%2000%3A02%3A03.4560")
}
func testThatDatesCanBeEncodedAsCustomFormatted() {
// Given
let encoder = URLEncodedFormEncoder(dateEncoding: .custom({ "\($0.timeIntervalSinceReferenceDate)" }))
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertEqual(result.value, "date=123.456")
}
func testEncoderThrowsErrorWhenCustomDateEncodingFails() {
// Given
struct DateEncodingError: Error {}
let encoder = URLEncodedFormEncoder(dateEncoding: .custom({ _ in throw DateEncodingError() }))
let parameters = ["date": Date(timeIntervalSinceReferenceDate: 123.456)]
// When
let result = AFResult<String> { try encoder.encode(parameters) }
// Then
XCTAssertTrue(result.isFailure)
XCTAssertTrue(result.error is DateEncodingError)
}
func testThatArraysCanBeEncodedWithoutBrackets() {
// Given
let encoder = URLEncodedFormEncoder(arrayEncoding: .noBrackets)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册