Skip to content

Commit

Permalink
Date encoding strategies for URLEncodedFormEncoder (Alamofire#2813)
Browse files Browse the repository at this point in the history
* Added date encoding strategies to `URLEncodedFormEncoder`

* Documentation fixes and error handling in `URLEncodedFormEncoder.DateEncoding`
  • Loading branch information
almazrafi authored and jshier committed May 7, 2019
1 parent 1081318 commit a516974
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 23 deletions.
142 changes: 119 additions & 23 deletions Source/ParameterEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 +.
///
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -342,34 +395,40 @@ 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
}
}

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)
}
}

Expand Down Expand Up @@ -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] {
Expand All @@ -522,33 +584,42 @@ 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
}

func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context,
codingPath: nestedCodingPath(for: key),
boolEncoding: boolEncoding)
boolEncoding: boolEncoding,
dateEncoding: dateEncoding)

return container
}

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)
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}

Expand All @@ -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
}
}
}
Expand All @@ -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)
}
Expand All @@ -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)
}
}

Expand Down
Loading

0 comments on commit a516974

Please sign in to comment.