|
|
@@ -26,19 +26,29 @@ import Foundation
|
|
|
|
|
|
/// An object that encodes instances into URL-encoded query strings.
|
|
|
///
|
|
|
-/// There is no published specification for how to encode collection types. By default, the convention of appending
|
|
|
-/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
|
|
|
-/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
|
|
|
-/// square brackets appended to array keys.
|
|
|
+/// `ArrayEncoding` can be used to configure how `Array` values are encoded. By default, the `.brackets` encoding is
|
|
|
+/// used, encoding array values with brackets for each value. e.g `array[]=1&array[]=2`.
|
|
|
///
|
|
|
-/// `BoolEncoding` can be used to configure how `Bool` values are encoded. The default behavior is to encode
|
|
|
-/// `true` as 1 and `false` as 0.
|
|
|
+/// `BoolEncoding` can be used to configure how `Bool` values are encoded. By default, the `.numeric` encoding is used,
|
|
|
+/// encoding `true` as `1` and `false` as `0`.
|
|
|
+///
|
|
|
+/// `DataEncoding` can be used to configure how `Data` values are encoded. By default, the `.deferredToData` encoding is
|
|
|
+/// used, which encodes `Data` values using their default `Encodable` implementation.
|
|
|
///
|
|
|
/// `DateEncoding` can be used to configure how `Date` values are encoded. By default, the `.deferredToDate`
|
|
|
-/// strategy is used, which formats dates from their structure.
|
|
|
+/// encoding is used, which encodes `Date`s using their default `Encodable` implementation.
|
|
|
+///
|
|
|
+/// `KeyEncoding` can be used to configure how keys are encoded. By default, the `.useDefaultKeys` encoding is used,
|
|
|
+/// which encodes the keys directly from the `Encodable` implementation.
|
|
|
+///
|
|
|
+/// `KeyPathEncoding` can be used to configure how paths within nested objects are encoded. By default, the `.brackets`
|
|
|
+/// encoding is used, which encodes each sub-key in brackets. e.g. `parent[child][grandchild]=value`.
|
|
|
+///
|
|
|
+/// `NilEncoding` can be used to configure how `nil` `Optional` values are encoded. By default, the `.dropKey` encoding
|
|
|
+/// is used, which drops `nil` key / value pairs from the output entirely.
|
|
|
///
|
|
|
-/// `SpaceEncoding` can be used to configure how spaces are encoded. Modern encodings use percent replacement (`%20`),
|
|
|
-/// while older encodings may expect spaces to be replaced with `+`.
|
|
|
+/// `SpaceEncoding` can be used to configure how spaces are encoded. By default, the `.percentEscaped` encoding is used,
|
|
|
+/// replacing spaces with `%20`.
|
|
|
///
|
|
|
/// This type is largely based on Vapor's [`url-encoded-form`](https://github.com/vapor/url-encoded-form) project.
|
|
|
public final class URLEncodedFormEncoder {
|
|
|
@@ -54,10 +64,10 @@ public final class URLEncodedFormEncoder {
|
|
|
/// Encodes the key according to the encoding.
|
|
|
///
|
|
|
/// - Parameters:
|
|
|
- /// - key: The `key` to encode.
|
|
|
- /// - index: When this enum instance is `.indexInBrackets`, the `index` to encode.
|
|
|
+ /// - key: The `key` to encode.
|
|
|
+ /// - index: When this enum instance is `.indexInBrackets`, the `index` to encode.
|
|
|
///
|
|
|
- /// - Returns: The encoded key.
|
|
|
+ /// - Returns: The encoded key.
|
|
|
func encode(_ key: String, atIndex index: Int) -> String {
|
|
|
switch self {
|
|
|
case .brackets: return "\(key)[]"
|
|
|
@@ -234,13 +244,13 @@ public final class URLEncodedFormEncoder {
|
|
|
var searchRange = key.index(after: wordStart)..<key.endIndex
|
|
|
|
|
|
// Find next uppercase character
|
|
|
- while let upperCaseRange = key.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
|
|
|
+ while let upperCaseRange = key.rangeOfCharacter(from: .uppercaseLetters, options: [], range: searchRange) {
|
|
|
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
|
|
|
words.append(untilUpperCase)
|
|
|
|
|
|
// Find next lowercase character
|
|
|
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
|
|
|
- guard let lowerCaseRange = key.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
|
|
|
+ guard let lowerCaseRange = key.rangeOfCharacter(from: .lowercaseLetters, options: [], range: searchRange) else {
|
|
|
// There are no more lower case letters. Just end here.
|
|
|
wordStart = searchRange.lowerBound
|
|
|
break
|
|
|
@@ -255,7 +265,8 @@ public final class URLEncodedFormEncoder {
|
|
|
// Continue searching for the next upper case for the boundary.
|
|
|
wordStart = upperCaseRange.lowerBound
|
|
|
} else {
|
|
|
- // There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
|
|
|
+ // There was a range of >1 capital letters. Turn those into a word, stopping at the capital before
|
|
|
+ // the lower case character.
|
|
|
let beforeLowerIndex = key.index(before: lowerCaseRange.lowerBound)
|
|
|
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
|
|
|
|
|
|
@@ -273,6 +284,34 @@ public final class URLEncodedFormEncoder {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Encoding to use for nested object and `Encodable` value key paths.
|
|
|
+ ///
|
|
|
+ /// ```
|
|
|
+ /// ["parent" : ["child" : ["grandchild": "value"]]]
|
|
|
+ /// ```
|
|
|
+ ///
|
|
|
+ /// This encoding affects how the `parent`, `child`, `grandchild` path is encoded. Brackets are used by default.
|
|
|
+ /// e.g. `parent[child][grandchild]=value`.
|
|
|
+ public struct KeyPathEncoding {
|
|
|
+ /// Encodes key paths by wrapping each component in brackets. e.g. `parent[child][grandchild]`.
|
|
|
+ public static let brackets = KeyPathEncoding { "[\($0)]" }
|
|
|
+ /// Encodes key paths by separating each component with dots. e.g. `parent.child.grandchild`.
|
|
|
+ public static let dots = KeyPathEncoding { ".\($0)" }
|
|
|
+
|
|
|
+ private let encoding: (_ subkey: String) -> String
|
|
|
+
|
|
|
+ /// Creates an instance with the encoding closure called for each sub-key in a key path.
|
|
|
+ ///
|
|
|
+ /// - Parameter encoding: Closure used to perform the encoding.
|
|
|
+ public init(encoding: @escaping (_ subkey: String) -> String) {
|
|
|
+ self.encoding = encoding
|
|
|
+ }
|
|
|
+
|
|
|
+ func encodeKeyPath(_ keyPath: String) -> String {
|
|
|
+ encoding(keyPath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/// Encoding to use for `nil` values.
|
|
|
public struct NilEncoding {
|
|
|
/// Encodes `nil` by dropping the entire key / value pair.
|
|
|
@@ -284,7 +323,7 @@ public final class URLEncodedFormEncoder {
|
|
|
|
|
|
private let encoding: () -> String?
|
|
|
|
|
|
- /// Creates an instance with the specified encoding.
|
|
|
+ /// Creates an instance with the encoding closure called for `nil` values.
|
|
|
///
|
|
|
/// - Parameter encoding: Closure used to perform the encoding.
|
|
|
public init(encoding: @escaping () -> String?) {
|
|
|
@@ -298,9 +337,9 @@ public final class URLEncodedFormEncoder {
|
|
|
|
|
|
/// Encoding to use for spaces.
|
|
|
public enum SpaceEncoding {
|
|
|
- /// Encodes spaces according to normal percent escaping rules (%20).
|
|
|
+ /// Encodes spaces using percent escaping (`%20`).
|
|
|
case percentEscaped
|
|
|
- /// Encodes spaces as `+`,
|
|
|
+ /// Encodes spaces as `+`.
|
|
|
case plusReplaced
|
|
|
|
|
|
/// Encodes the string according to the encoding.
|
|
|
@@ -345,6 +384,8 @@ public final class URLEncodedFormEncoder {
|
|
|
public let dateEncoding: DateEncoding
|
|
|
/// The `KeyEncoding` to use.
|
|
|
public let keyEncoding: KeyEncoding
|
|
|
+ /// The `KeyPathEncoding` to use.
|
|
|
+ public let keyPathEncoding: KeyPathEncoding
|
|
|
/// The `NilEncoding` to use.
|
|
|
public let nilEncoding: NilEncoding
|
|
|
/// The `SpaceEncoding` to use.
|
|
|
@@ -371,6 +412,7 @@ public final class URLEncodedFormEncoder {
|
|
|
dataEncoding: DataEncoding = .base64,
|
|
|
dateEncoding: DateEncoding = .deferredToDate,
|
|
|
keyEncoding: KeyEncoding = .useDefaultKeys,
|
|
|
+ keyPathEncoding: KeyPathEncoding = .brackets,
|
|
|
nilEncoding: NilEncoding = .dropKey,
|
|
|
spaceEncoding: SpaceEncoding = .percentEscaped,
|
|
|
allowedCharacters: CharacterSet = .afURLQueryAllowed) {
|
|
|
@@ -380,6 +422,7 @@ public final class URLEncodedFormEncoder {
|
|
|
self.dataEncoding = dataEncoding
|
|
|
self.dateEncoding = dateEncoding
|
|
|
self.keyEncoding = keyEncoding
|
|
|
+ self.keyPathEncoding = keyPathEncoding
|
|
|
self.nilEncoding = nilEncoding
|
|
|
self.spaceEncoding = spaceEncoding
|
|
|
self.allowedCharacters = allowedCharacters
|
|
|
@@ -413,6 +456,7 @@ public final class URLEncodedFormEncoder {
|
|
|
let serializer = URLEncodedFormSerializer(alphabetizeKeyValuePairs: alphabetizeKeyValuePairs,
|
|
|
arrayEncoding: arrayEncoding,
|
|
|
keyEncoding: keyEncoding,
|
|
|
+ keyPathEncoding: keyPathEncoding,
|
|
|
spaceEncoding: spaceEncoding,
|
|
|
allowedCharacters: allowedCharacters)
|
|
|
let query = serializer.serialize(object)
|
|
|
@@ -942,17 +986,20 @@ final class URLEncodedFormSerializer {
|
|
|
private let alphabetizeKeyValuePairs: Bool
|
|
|
private let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding
|
|
|
private let keyEncoding: URLEncodedFormEncoder.KeyEncoding
|
|
|
+ private let keyPathEncoding: URLEncodedFormEncoder.KeyPathEncoding
|
|
|
private let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding
|
|
|
private let allowedCharacters: CharacterSet
|
|
|
|
|
|
init(alphabetizeKeyValuePairs: Bool,
|
|
|
arrayEncoding: URLEncodedFormEncoder.ArrayEncoding,
|
|
|
keyEncoding: URLEncodedFormEncoder.KeyEncoding,
|
|
|
+ keyPathEncoding: URLEncodedFormEncoder.KeyPathEncoding,
|
|
|
spaceEncoding: URLEncodedFormEncoder.SpaceEncoding,
|
|
|
allowedCharacters: CharacterSet) {
|
|
|
self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs
|
|
|
self.arrayEncoding = arrayEncoding
|
|
|
self.keyEncoding = keyEncoding
|
|
|
+ self.keyPathEncoding = keyPathEncoding
|
|
|
self.spaceEncoding = spaceEncoding
|
|
|
self.allowedCharacters = allowedCharacters
|
|
|
}
|
|
|
@@ -978,7 +1025,7 @@ final class URLEncodedFormSerializer {
|
|
|
|
|
|
func serialize(_ object: URLEncodedFormComponent.Object, forKey key: String) -> String {
|
|
|
var segments: [String] = object.map { subKey, value in
|
|
|
- let keyPath = "[\(subKey)]"
|
|
|
+ let keyPath = keyPathEncoding.encodeKeyPath(subKey)
|
|
|
return serialize(value, forKey: key + keyPath)
|
|
|
}
|
|
|
segments = alphabetizeKeyValuePairs ? segments.sorted() : segments
|