Permalink
Browse files

Add configurable parameter handling to URLEncoding. (#2431)

* Add configurable array parameter handling to URLEncoding.

* Add configurable boolean parameter handling to URLEncoding.

* Addressing PR feedback for URLEncoding.

* Documentation for BoolEncoding and ArrayEncoding options in URLEncoding.
  • Loading branch information...
heiberg authored and jshier committed Feb 20, 2018
1 parent 4df43fb commit 0456f5b501f46591a77b4bb6f2e2e2a97d095042
Showing with 155 additions and 7 deletions.
  1. +32 −0 Documentation/Usage.md
  2. +58 −7 Source/ParameterEncoding.swift
  3. +65 −0 Tests/ParameterEncodingTests.swift
@@ -279,6 +279,38 @@ Alamofire.request("https://httpbin.org/post", method: .post, parameters: paramet
// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
```
##### Configuring the Encoding of `Bool` Parameters
The `URLEncoding.BoolEncoding` enumeration provides the following methods for encoding `Bool` parameters:
- `.numeric` - Encode `true` as `1` and `false` as `0`.
- `.literal` - Encode `true` and `false` as string literals.
By default, Alamofire uses the `.numeric` encoding.
You can create your own `URLEncoding` and specify the desired `Bool` encoding in the initializer:
```swift
let encoding = URLEncoding(boolEncoding: .literal)
```
##### Configuring the Encoding of `Array` Parameters
The `URLEncoding.ArrayEncoding` enumeration provides the following methods for encoding `Array` parameters:
- `.brackets` - An empty set of square brackets is appended to the key for every value.
- `.noBrackets` - No brackets are appended. The key is encoded as is.
By default, Alamofire uses the `.brackets` encoding, where `foo=[1,2]` is encoded as `foo[]=1&foo[]=2`.
Using the `.noBrackets` encoding will encode `foo=[1,2]` as `foo=1&foo=2`.
You can create your own `URLEncoding` and specify the desired `Array` encoding in the initializer:
```swift
let encoding = URLEncoding(arrayEncoding: .noBrackets)
```
#### JSON Encoding
The `JSONEncoding` type creates a JSON representation of the parameters object, which is set as the HTTP body of the request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
@@ -64,9 +64,15 @@ public protocol ParameterEncoding {
/// the HTTP body depends on the destination of the encoding.
///
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
/// `application/x-www-form-urlencoded; charset=utf-8`. Since there is no published specification for how to encode
/// collection types, 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`).
/// `application/x-www-form-urlencoded; charset=utf-8`.
///
/// 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.
///
/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
/// `true` as 1 and `false` as 0.
public struct URLEncoding: ParameterEncoding {
// MARK: Helper Types
@@ -82,6 +88,41 @@ public struct URLEncoding: ParameterEncoding {
case methodDependent, queryString, httpBody
}
/// Configures how `Array` parameters are encoded.
///
/// - brackets: An empty set of square brackets is appended to the key for every value.
/// This is the default behavior.
/// - noBrackets: No brackets are appended. The key is encoded as is.
public enum ArrayEncoding {
case brackets, noBrackets
func encode(key: String) -> String {
switch self {
case .brackets:
return "\(key)[]"
case .noBrackets:
return key
}
}
}
/// Configures how `Bool` parameters are encoded.
///
/// - numeric: Encode `true` as `1` and `false` as `0`. This is the default behavior.
/// - literal: Encode `true` and `false` as string literals.
public enum BoolEncoding {
case numeric, literal
func encode(value: Bool) -> String {
switch self {
case .numeric:
return value ? "1" : "0"
case .literal:
return value ? "true" : "false"
}
}
}
// MARK: Properties
/// Returns a default `URLEncoding` instance.
@@ -99,15 +140,25 @@ public struct URLEncoding: ParameterEncoding {
/// The destination defining where the encoded query string is to be applied to the URL request.
public let destination: Destination
/// The encoding to use for `Array` parameters.
public let arrayEncoding: ArrayEncoding
/// The encoding to use for `Bool` parameters.
public let boolEncoding: BoolEncoding
// MARK: Initialization
/// Creates a `URLEncoding` instance using the specified destination.
///
/// - parameter destination: The destination defining where the encoded query string is to be applied.
/// - parameter arrayEncoding: The encoding to use for `Array` parameters.
/// - parameter boolEncoding: The encoding to use for `Bool` parameters.
///
/// - returns: The new `URLEncoding` instance.
public init(destination: Destination = .methodDependent) {
public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
self.destination = destination
self.arrayEncoding = arrayEncoding
self.boolEncoding = boolEncoding
}
// MARK: Encoding
@@ -161,16 +212,16 @@ public struct URLEncoding: ParameterEncoding {
}
} else if let array = value as? [Any] {
for value in array {
components += queryComponents(fromKey: "\(key)[]", value: value)
components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
}
} else if let value = value as? NSNumber {
if value.isBool {
components.append((escape(key), escape((value.boolValue ? "1" : "0"))))
components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
} else {
components.append((escape(key), escape("\(value)")))
}
} else if let bool = value as? Bool {
components.append((escape(key), escape((bool ? "1" : "0"))))
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
} else {
components.append((escape(key), escape("\(value)")))
}
@@ -207,6 +207,22 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
}
}
func testURLParameterEncodeStringKeyArrayValueParameterWithoutBrackets() {
do {
// Given
let encoding = URLEncoding(arrayEncoding: .noBrackets)
let parameters = ["foo": ["a", 1, true]]
// When
let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
// Then
XCTAssertEqual(urlRequest.url?.query, "foo=a&foo=1&foo=1")
} catch {
XCTFail("Test encountered unexpected error: \(error)")
}
}
func testURLParameterEncodeStringKeyDictionaryValueParameter() {
do {
// Given
@@ -253,6 +269,55 @@ class URLParameterEncodingTestCase: ParameterEncodingTestCase {
}
}
func testURLParameterEncodeStringKeyNestedDictionaryArrayValueParameterWithoutBrackets() {
do {
// Given
let encoding = URLEncoding(arrayEncoding: .noBrackets)
let parameters = ["foo": ["bar": ["baz": ["a", 1, true]]]]
// When
let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
// Then
let expectedQuery = "foo%5Bbar%5D%5Bbaz%5D=a&foo%5Bbar%5D%5Bbaz%5D=1&foo%5Bbar%5D%5Bbaz%5D=1"
XCTAssertEqual(urlRequest.url?.query, expectedQuery)
} catch {
XCTFail("Test encountered unexpected error: \(error)")
}
}
func testURLParameterLiteralBoolEncodingWorksAndDoesNotAffectNumbers() {
do {
// Given
let encoding = URLEncoding(boolEncoding: .literal)
let parameters: [String: Any] = [
// Must still encode to numbers
"a": 1,
"b": 0,
"c": 1.0,
"d": 0.0,
"e": NSNumber(value: 1),
"f": NSNumber(value: 0),
"g": NSNumber(value: 1.0),
"h": NSNumber(value: 0.0),
// Must encode to literals
"i": true,
"j": false,
"k": NSNumber(value: true),
"l": NSNumber(value: false)
]
// When
let urlRequest = try encoding.encode(self.urlRequest, with: parameters)
// Then
XCTAssertEqual(urlRequest.url?.query, "a=1&b=0&c=1&d=0&e=1&f=0&g=1&h=0&i=true&j=false&k=true&l=false")
} catch {
XCTFail("Test encountered unexpected error: \(error)")
}
}
// MARK: Tests - All Reserved / Unreserved / Illegal Characters According to RFC 3986
func testThatReservedCharactersArePercentEscapedMinusQuestionMarkAndForwardSlash() {

0 comments on commit 0456f5b

Please sign in to comment.