Selaa lähdekoodia

Stable encoded parameter order for URLEncodedFormEncoder (#2961)

* Update URLEncodedFormEncoder for fixed order.

* Update GitHub CI configuration.

* Update CI config again.

* Remove now unnecessary assertQueryEqual.

* Use Xcode 11 for all builds.

* Run things through xcpretty.

* Stop using Travis.

* Update piping.

* Sort cURL component comparisons.

* Increase timeout for test resiliancy.

* Update Source/URLEncodedFormEncoder.swift

* Update comment.

* Move to Xcode 11.1 for CI.
Jon Shier 6 vuotta sitten
vanhempi
commit
9187007b0f

+ 23 - 12
.github/workflows/ci.yml

@@ -2,55 +2,66 @@ name: "Alamofire CI"
 
 on: 
   push:
-    branches:
+    branches: 
       - master
+      - hotfix
+  pull_request:
+    branches: 
+      - master
+      - hotfix
 
 jobs:
   macOS:
     name: Test macOS 
     runs-on: macOS-latest
+    env: 
+      DEVELOPER_DIR: /Applications/Xcode_11.1.app/Contents/Developer
     steps:
       - uses: actions/checkout@v1    
       - name: macOS
-        run: xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire macOS" -destination "platform=macOS" clean test
+        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire macOS" -destination "platform=macOS" clean test | xcpretty
   iOS:
     name: Test iOS 
     runs-on: macOS-latest
+    env: 
+      DEVELOPER_DIR: /Applications/Xcode_11.1.app/Contents/Developer
     strategy:
       matrix:
-        destination: ["OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
+        destination: ["OS=13.1,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
     steps:
       - uses: actions/checkout@v1            
       - name: iOS - ${{ matrix.destination }}
-        run: xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test
+        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test | xcpretty
   tvOS:
     name: Test tvOS 
     runs-on: macOS-latest
+    env: 
+      DEVELOPER_DIR: /Applications/Xcode_11.1.app/Contents/Developer
     strategy:
       matrix:
-        destination: ["OS=12.4,name=Apple TV 4K", "OS=11.4,name=Apple TV 4K", "OS=10.2,name=Apple TV 1080p"]
+        destination: ["OS=13.0,name=Apple TV 4K"] #, "OS=11.4,name=Apple TV 4K", "OS=10.2,name=Apple TV 1080p"]
     steps:
       - uses: actions/checkout@v1            
       - name: tvOS - ${{ matrix.destination }}
-        run: xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire tvOS" -destination "${{ matrix.destination }}" clean test
+        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire tvOS" -destination "${{ matrix.destination }}" clean test | xcpretty
   watchOS:
     name: Build watchOS
     runs-on: macOS-latest
+    env: 
+      DEVELOPER_DIR: /Applications/Xcode_11.1.app/Contents/Developer
     strategy:
       matrix:
-        destination: ["OS=5.3,name=Apple Watch Series 4 - 44mm", "OS=4.2,name=Apple Watch Series 3 - 42mm", "OS=3.2,name=Apple Watch Series 2 - 42mm"]
+        destination: ["OS=6.0,name=Apple Watch Series 5 - 44mm"] #, "OS=4.2,name=Apple Watch Series 3 - 42mm", "OS=3.2,name=Apple Watch Series 2 - 42mm"]
     steps:
       - uses: actions/checkout@v1
       - name: watchOS - ${{ matrix.destination }}
-        run: xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire watchOS" -destination "${{ matrix.destination }}" clean build
+        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire watchOS" -destination "${{ matrix.destination }}" clean build | xcpretty
   spm:
     name: Test SPM Integration
     runs-on: macOS-latest    
     needs: [macOS]
     steps:
       - uses: actions/checkout@v1
-      - name: SPM Build & Test
-        run: |
-          swift build
-          swift test
+      - name: SPM Build
+        run: swift build
 

+ 0 - 51
.travis.yml

@@ -1,51 +0,0 @@
-os: osx
-osx_image: xcode10.2
-branches:
-  only:
-    - master
-cache: bundler
-env:
-  global:
-  - LC_CTYPE=en_US.UTF-8
-  - LANG=en_US.UTF-8
-  - WORKSPACE=Alamofire.xcworkspace
-  - IOS_FRAMEWORK_SCHEME="Alamofire iOS"
-  - MACOS_FRAMEWORK_SCHEME="Alamofire macOS"
-  - TVOS_FRAMEWORK_SCHEME="Alamofire tvOS"
-  - WATCHOS_FRAMEWORK_SCHEME="Alamofire watchOS"
-  - EXAMPLE_SCHEME="iOS Example"
-  matrix:
-    - DESTINATION="OS=5.2,name=Apple Watch Series 4 - 44mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
-    - DESTINATION="OS=4.2,name=Apple Watch Series 3 - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
-    - DESTINATION="OS=3.2,name=Apple Watch Series 2 - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
-
-    - DESTINATION="OS=12.2,name=iPhone XS"       SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO"
-    - DESTINATION="OS=11.4,name=iPhone X"        SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO"
-    - DESTINATION="OS=10.3.1,name=iPhone 7 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO"
-
-    - DESTINATION="OS=12.2,name=Apple TV 4K"    SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO"
-    - DESTINATION="OS=11.4,name=Apple TV 4K"    SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO"
-    - DESTINATION="OS=10.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO"
-
-    - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="YES"
-script:
-  - set -o pipefail
-  - xcodebuild -version
-  - xcodebuild -showsdks
-
-  # Build Framework in Release and Run Tests if specified
-  - if [ $RUN_TESTS == "YES" ]; then
-      xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
-    else
-      xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
-    fi
-
-  # Build Example in Debug if specified
-  - if [ $BUILD_EXAMPLE == "YES" ]; then
-      xcodebuild -workspace "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
-    fi
-
-  # Run `pod lib lint` if specified
-  - if [ $POD_LINT == "YES" ]; then
-      pod lib lint;
-    fi

+ 0 - 7
Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme

@@ -53,13 +53,6 @@
             ReferencedContainer = "container:Alamofire.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <EnvironmentVariables>
-         <EnvironmentVariable
-            key = "SWIFT_DETERMINISTIC_HASHING"
-            value = "1"
-            isEnabled = "YES">
-         </EnvironmentVariable>
-      </EnvironmentVariables>
       <AdditionalOptions>
          <AdditionalOption
             key = "NSZombieEnabled"

+ 0 - 7
Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme

@@ -53,13 +53,6 @@
             ReferencedContainer = "container:Alamofire.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <EnvironmentVariables>
-         <EnvironmentVariable
-            key = "SWIFT_DETERMINISTIC_HASHING"
-            value = "1"
-            isEnabled = "YES">
-         </EnvironmentVariable>
-      </EnvironmentVariables>
       <AdditionalOptions>
          <AdditionalOption
             key = "NSZombieEnabled"

+ 0 - 7
Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire tvOS.xcscheme

@@ -53,13 +53,6 @@
             ReferencedContainer = "container:Alamofire.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <EnvironmentVariables>
-         <EnvironmentVariable
-            key = "SWIFT_DETERMINISTIC_HASHING"
-            value = "1"
-            isEnabled = "YES">
-         </EnvironmentVariable>
-      </EnvironmentVariables>
       <AdditionalOptions>
          <AdditionalOption
             key = "NSZombieEnabled"

+ 11 - 0
Documentation/Usage.md

@@ -7,6 +7,7 @@
       - [`URLEncodedFormParameterEncoder`](#-urlencodedformparameterencoder-)
         * [GET Request With URL-Encoded Parameters](#get-request-with-url-encoded-parameters)
         * [POST Request With URL-Encoded Parameters](#post-request-with-url-encoded-parameters)
+        * [Configuring the Sorting of Encoded Parameters](#configuring-the-sorting-of-encoded-parameters)
         * [Configuring the Encoding of `Array` Parameters](#configuring-the-encoding-of--array--parameters)
         * [Configuring the Encoding of `Bool` Parameters](#configuring-the-encoding-of--bool--parameters)
         * [Configuring the Encoding of `Data` Parameters](#configuring-the-encoding-of--data--parameters)
@@ -208,6 +209,16 @@ AF.request("https://httpbin.org/post", method: .post, parameters: parameters, en
 // HTTP body: "qux[]=x&qux[]=y&qux[]=z&baz[]=a&baz[]=b&foo[]=bar"
 ```
 
+#### Configuring the Sorting of Encoded Values
+
+Since Swift 4.2, the hashing algorithm used by Swift's `Dictionary` type produces a random internal ordering at runtime which differs between app launches. This can cause encoded parameters to change order, which may have an impact on caching and other behaviors. By default `URLEncodedFormEncoder` will sort its encoded key-value pairs. While this produces constant output for all `Encodable` types, it may not match the actual encoding order implemented by the type. You can set `alphabetizeKeyValuePairs` to return to implementation order, though that will also have the randomized `Dictionary` order as well.
+
+You can create your own `URLEncodedFormParameterEncoder` and specify the desired `alphabetizeKeyValuePairs` in the initializer of the passed `URLEncodedFormEncoder`:
+
+```swift
+let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(alphabetizeKeyValuePairs: false))
+```
+
 ##### Configuring the Encoding of `Array` Parameters
 
 Since there is no published specification for how to encode collection types, by default Alamofire follows 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`).

+ 11 - 12
Source/Request.swift

@@ -859,26 +859,25 @@ extension Request {
             }
         }
 
-        var headers: [String: String] = [:]
+        var headers = HTTPHeaders()
 
-        if let additionalHeaders = delegate?.sessionConfiguration.httpAdditionalHeaders as? [String: String] {
-            for (field, value) in additionalHeaders where field != "Cookie" {
-                headers[field] = value
+        if let sessionHeaders = delegate?.sessionConfiguration.headers {
+            for header in sessionHeaders where header.name != "Cookie" {
+                headers[header.name] = header.value
             }
         }
 
-        if let headerFields = request.allHTTPHeaderFields {
-            for (field, value) in headerFields where field != "Cookie" {
-                headers[field] = value
-            }
+        for header in request.headers where header.name != "Cookie" {
+            headers[header.name] = header.value
         }
 
-        for (field, value) in headers {
-            let escapedValue = value.replacingOccurrences(of: "\"", with: "\\\"")
-            components.append("-H \"\(field): \(escapedValue)\"")
+        for header in headers {
+            let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"")
+            components.append("-H \"\(header.name): \(escapedValue)\"")
         }
 
-        if let httpBodyData = request.httpBody, let httpBody = String(data: httpBodyData, encoding: .utf8) {
+        if let httpBodyData = request.httpBody {
+            let httpBody = String(decoding: httpBodyData, as: UTF8.self)
             var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"")
             escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"")
 

+ 56 - 32
Source/URLEncodedFormEncoder.swift

@@ -177,17 +177,17 @@ public final class URLEncodedFormEncoder {
         ///
         /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
         case convertToSnakeCase
-        /// Same as convertToSnakeCase, but using `-` instead of `_`
+        /// Same as convertToSnakeCase, but using `-` instead of `_`.
         /// For example `oneTwoThree` becomes `one-two-three`.
         case convertToKebabCase
-        /// Capitalize the first letter only
-        /// For example `oneTwoThree` becomes  `OneTwoThree`
+        /// Capitalize the first letter only.
+        /// For example `oneTwoThree` becomes  `OneTwoThree`.
         case capitalized
-        /// Uppercase all letters
-        /// For example `oneTwoThree` becomes  `ONETWOTHREE`
+        /// Uppercase all letters.
+        /// For example `oneTwoThree` becomes  `ONETWOTHREE`.
         case uppercased
-        /// Lowercase all letters
-        /// For example `oneTwoThree` becomes  `onetwothree`
+        /// Lowercase all letters.
+        /// For example `oneTwoThree` becomes  `onetwothree`.
         case lowercased
         /// A custom encoding using the provided closure.
         case custom((String) -> String)
@@ -294,11 +294,18 @@ public final class URLEncodedFormEncoder {
 
         var localizedDescription: String {
             switch self {
-            case let .invalidRootObject(object): return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead."
+            case let .invalidRootObject(object):
+                return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead."
             }
         }
     }
 
+    /// Whether or not to sort the encoded key value pairs.
+    ///
+    /// - Note: This setting ensures a consistent ordering for all encodings of the same parameters. When set to `false`,
+    ///         encoded `Dictionary` values may have a different encoded order each time they're encoded due to
+    ///       ` Dictionary`'s random storage order, but `Encodable` types will maintain their encoded order.
+    public let alphabetizeKeyValuePairs: Bool
     /// The `ArrayEncoding` to use.
     public let arrayEncoding: ArrayEncoding
     /// The `BoolEncoding` to use.
@@ -317,20 +324,24 @@ public final class URLEncodedFormEncoder {
     /// Creates an instance from the supplied parameters.
     ///
     /// - Parameters:
-    ///   - arrayEncoding:     The `ArrayEncoding` to use. `.brackets` by default.
-    ///   - boolEncoding:      The `BoolEncoding` to use. `.numeric` by default.
-    ///   - dataEncoding:      The `DataEncoding` to use. `.base64` by default.
-    ///   - dateEncoding:      The `DateEncoding` to use. `.deferredToDate` by default.
-    ///   - keyEncoding:       The `KeyEncoding` to use. `.useDefaultKeys` by default.
-    ///   - spaceEncoding:     The `SpaceEncoding` to use. `.percentEscaped` by default.
-    ///   - allowedCharacters: The `CharacterSet` of allowed (non-escaped) characters. `.afURLQueryAllowed` by default.
-    public init(arrayEncoding: ArrayEncoding = .brackets,
+    ///   - alphabetizeKeyValuePairs: Whether or not to sort the encoded key value pairs. `true` by default.
+    ///   - arrayEncoding:            The `ArrayEncoding` to use. `.brackets` by default.
+    ///   - boolEncoding:             The `BoolEncoding` to use. `.numeric` by default.
+    ///   - dataEncoding:             The `DataEncoding` to use. `.base64` by default.
+    ///   - dateEncoding:             The `DateEncoding` to use. `.deferredToDate` by default.
+    ///   - keyEncoding:              The `KeyEncoding` to use. `.useDefaultKeys` by default.
+    ///   - spaceEncoding:            The `SpaceEncoding` to use. `.percentEscaped` by default.
+    ///   - allowedCharacters:        The `CharacterSet` of allowed (non-escaped) characters. `.afURLQueryAllowed` by
+    ///                               default.
+    public init(alphabetizeKeyValuePairs: Bool = true,
+                arrayEncoding: ArrayEncoding = .brackets,
                 boolEncoding: BoolEncoding = .numeric,
                 dataEncoding: DataEncoding = .base64,
                 dateEncoding: DateEncoding = .deferredToDate,
                 keyEncoding: KeyEncoding = .useDefaultKeys,
                 spaceEncoding: SpaceEncoding = .percentEscaped,
                 allowedCharacters: CharacterSet = .afURLQueryAllowed) {
+        self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs
         self.arrayEncoding = arrayEncoding
         self.boolEncoding = boolEncoding
         self.dataEncoding = dataEncoding
@@ -341,7 +352,7 @@ public final class URLEncodedFormEncoder {
     }
 
     func encode(_ value: Encodable) throws -> URLEncodedFormComponent {
-        let context = URLEncodedFormContext(.object([:]))
+        let context = URLEncodedFormContext(.object([]))
         let encoder = _URLEncodedFormEncoder(context: context,
                                              boolEncoding: boolEncoding,
                                              dataEncoding: dataEncoding,
@@ -364,7 +375,8 @@ public final class URLEncodedFormEncoder {
             throw Error.invalidRootObject("\(component)")
         }
 
-        let serializer = URLEncodedFormSerializer(arrayEncoding: arrayEncoding,
+        let serializer = URLEncodedFormSerializer(alphabetizeKeyValuePairs: alphabetizeKeyValuePairs,
+                                                  arrayEncoding: arrayEncoding,
                                                   keyEncoding: keyEncoding,
                                                   spaceEncoding: spaceEncoding,
                                                   allowedCharacters: allowedCharacters)
@@ -448,9 +460,11 @@ final class URLEncodedFormContext {
 }
 
 enum URLEncodedFormComponent {
+    typealias Object = [(key: String, value: URLEncodedFormComponent)]
+
     case string(String)
     case array([URLEncodedFormComponent])
-    case object([String: URLEncodedFormComponent])
+    case object(Object)
 
     /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible.
     var array: [URLEncodedFormComponent]? {
@@ -460,8 +474,8 @@ enum URLEncodedFormComponent {
         }
     }
 
-    /// Converts self to an `[String: URLEncodedFormData]` or returns `nil` if not convertible.
-    var object: [String: URLEncodedFormComponent]? {
+    /// Converts self to an `Object` or returns `nil` if not convertible.
+    var object: Object? {
         switch self {
         case let .object(object): return object
         default: return nil
@@ -501,7 +515,7 @@ enum URLEncodedFormComponent {
                 }
                 set(&child, to: value, at: Array(path[1...]))
             } else {
-                child = context.object?[end.stringValue] ?? .object([:])
+                child = context.object?.first { $0.key == end.stringValue }?.value ?? .object(.init())
                 set(&child, to: value, at: Array(path[1...]))
             }
         default: fatalError("Unreachable")
@@ -520,10 +534,14 @@ enum URLEncodedFormComponent {
             }
         } else {
             if var object = context.object {
-                object[end.stringValue] = child
+                if let index = object.firstIndex(where: { $0.key == end.stringValue }) {
+                    object[index] = (key: end.stringValue, value: child)
+                } else {
+                    object.append((key: end.stringValue, value: child))
+                }
                 context = .object(object)
             } else {
-                context = .object([end.stringValue: child])
+                context = .object([(key: end.stringValue, value: child)])
             }
         }
     }
@@ -861,27 +879,31 @@ extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
 }
 
 final class URLEncodedFormSerializer {
+    private let alphabetizeKeyValuePairs: Bool
     private let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding
     private let keyEncoding: URLEncodedFormEncoder.KeyEncoding
     private let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding
     private let allowedCharacters: CharacterSet
 
-    init(arrayEncoding: URLEncodedFormEncoder.ArrayEncoding,
+    init(alphabetizeKeyValuePairs: Bool,
+         arrayEncoding: URLEncodedFormEncoder.ArrayEncoding,
          keyEncoding: URLEncodedFormEncoder.KeyEncoding,
          spaceEncoding: URLEncodedFormEncoder.SpaceEncoding,
          allowedCharacters: CharacterSet) {
+        self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs
         self.arrayEncoding = arrayEncoding
         self.keyEncoding = keyEncoding
         self.spaceEncoding = spaceEncoding
         self.allowedCharacters = allowedCharacters
     }
 
-    func serialize(_ object: [String: URLEncodedFormComponent]) -> String {
+    func serialize(_ object: URLEncodedFormComponent.Object) -> String {
         var output: [String] = []
         for (key, component) in object {
             let value = serialize(component, forKey: key)
             output.append(value)
         }
+        output = alphabetizeKeyValuePairs ? output.sorted() : output
 
         return output.joinedWithAmpersands()
     }
@@ -890,24 +912,26 @@ final class URLEncodedFormSerializer {
         switch component {
         case let .string(string): return "\(escape(keyEncoding.encode(key)))=\(escape(string))"
         case let .array(array): return serialize(array, forKey: key)
-        case let .object(dictionary): return serialize(dictionary, forKey: key)
+        case let .object(object): return serialize(object, forKey: key)
         }
     }
 
-    func serialize(_ object: [String: URLEncodedFormComponent], forKey key: String) -> String {
-        let segments: [String] = object.map { subKey, value in
+    func serialize(_ object: URLEncodedFormComponent.Object, forKey key: String) -> String {
+        var segments: [String] = object.map { subKey, value in
             let keyPath = "[\(subKey)]"
             return serialize(value, forKey: key + keyPath)
         }
+        segments = alphabetizeKeyValuePairs ? segments.sorted() : segments
 
         return segments.joinedWithAmpersands()
     }
 
     func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String {
-        let segments: [String] = array.map { component in
+        var segments: [String] = array.map { component in
             let keyPath = arrayEncoding.encode(key)
             return serialize(component, forKey: keyPath)
         }
+        segments = alphabetizeKeyValuePairs ? segments.sorted() : segments
 
         return segments.joinedWithAmpersands()
     }
@@ -928,7 +952,7 @@ extension Array where Element == String {
     }
 }
 
-extension CharacterSet {
+public extension CharacterSet {
     /// Creates a CharacterSet from RFC 3986 allowed characters.
     ///
     /// RFC 3986 states that the following characters are "reserved" characters.
@@ -939,7 +963,7 @@ extension CharacterSet {
     /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
     /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
     /// should be percent-escaped in the query string.
-    public static let afURLQueryAllowed: CharacterSet = {
+    static let afURLQueryAllowed: CharacterSet = {
         let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
         let subDelimitersToEncode = "!$&'()*+,;="
         let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")

+ 1 - 1
Tests/BaseTestCase.swift

@@ -27,7 +27,7 @@ import Foundation
 import XCTest
 
 class BaseTestCase: XCTestCase {
-    let timeout: TimeInterval = 5
+    let timeout: TimeInterval = 10
 
     static var testDirectoryURL: URL { return FileManager.temporaryDirectoryURL.appendingPathComponent("org.alamofire.tests") }
     var testDirectoryURL: URL { return BaseTestCase.testDirectoryURL }

+ 45 - 39
Tests/ParameterEncoderTests.swift

@@ -382,8 +382,8 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        let expected = "four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&three=1&one=one&two=2&five%5Ba%5D=a&six%5Ba%5D%5Bb%5D=b&seven%5Ba%5D=a"
-        XCTAssertQueryEqual(result.success, expected)
+        let expected = "five%5Ba%5D=a&four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&one=one&seven%5Ba%5D=a&six%5Ba%5D%5Bb%5D=b&three=1&two=2"
+        XCTAssertEqual(result.success, expected)
     }
 
     func testThatManuallyEncodableStructCanBeEncoded() {
@@ -395,8 +395,8 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        let expected = "root%5B%5D%5B%5D=1&root%5B%5D%5B%5D=2&root%5B%5D%5B%5D=3&root%5B%5D%5Ba%5D%5Bstring%5D=string&root%5B%5D%5B%5D%5B%5D=1&root%5B%5D%5B%5D%5B%5D=2&root%5B%5D%5B%5D%5B%5D=3"
-        XCTAssertQueryEqual(result.success, expected)
+        let expected = "root%5B%5D%5B%5D%5B%5D=1&root%5B%5D%5B%5D%5B%5D=2&root%5B%5D%5B%5D%5B%5D=3&root%5B%5D%5B%5D=1&root%5B%5D%5B%5D=2&root%5B%5D%5B%5D=3&root%5B%5D%5Ba%5D%5Bstring%5D=string"
+        XCTAssertEqual(result.success, expected)
     }
 
     func testThatEncodableClassWithNoInheritanceCanBeEncoded() {
@@ -408,7 +408,7 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        XCTAssertQueryEqual(result.success, "two=2&one=one&three=1")
+        XCTAssertEqual(result.success, "one=one&three=1&two=2")
     }
 
     func testThatEncodableSubclassCanBeEncoded() {
@@ -420,8 +420,21 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        let expected = "four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&two=2&five%5Ba%5D=a&five%5Bb%5D=b&three=1&one=one"
-        XCTAssertQueryEqual(result.success, expected)
+        let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&one=one&three=1&two=2"
+        XCTAssertEqual(result.success, expected)
+    }
+
+    func testThatEncodableSubclassCanBeEncodedInImplementationOrderWhenAlphabetizeKeysIsFalse() {
+        // Given
+        let encoder = URLEncodedFormEncoder(alphabetizeKeyValuePairs: false)
+        let parameters = EncodableStruct()
+
+        // When
+        let result = Result<String, Error> { try encoder.encode(parameters) }
+
+        // Then
+        let expected = "one=one&two=2&three=1&four%5B%5D=1&four%5B%5D=2&four%5B%5D=3&five%5Ba%5D=a&six%5Ba%5D%5Bb%5D=b&seven%5Ba%5D=a"
+        XCTAssertEqual(result.success, expected)
     }
 
     func testThatManuallyEncodableSubclassCanBeEncoded() {
@@ -433,8 +446,8 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5Bfour%5D=one&four%5Bfive%5D=2"
-        XCTAssertQueryEqual(result.success, expected)
+        let expected = "five%5Ba%5D=a&five%5Bb%5D=b&four%5Bfive%5D=2&four%5Bfour%5D=one"
+        XCTAssertEqual(result.success, expected)
     }
 
     func testThatARootArrayCannotBeEncoded() {
@@ -462,7 +475,7 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     }
 
     func testThatOptionalValuesCannotBeEncoded() {
-        // Givenp
+        // Given
         let encoder = URLEncodedFormEncoder()
         let parameters: [String: String?] = ["string": nil]
 
@@ -482,7 +495,7 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        XCTAssertQueryEqual(result.success, "array=1&array=2")
+        XCTAssertEqual(result.success, "array=1&array=2")
     }
 
     func testThatBoolsCanBeLiteralEncoded() {
@@ -618,10 +631,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeEncodedIntoSnakeCase() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .convertToSnakeCase)
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "one_two_three=oneTwoThree")
@@ -630,10 +643,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeEncodedIntoKebabCase() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .convertToKebabCase)
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "one-two-three=oneTwoThree")
@@ -642,10 +655,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeEncodedIntoACapitalizedString() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .capitalized)
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "OneTwoThree=oneTwoThree")
@@ -654,10 +667,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeEncodedIntoALowercasedString() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .lowercased)
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "onetwothree=oneTwoThree")
@@ -666,10 +679,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeEncodedIntoAnUppercasedString() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .uppercased)
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "ONETWOTHREE=oneTwoThree")
@@ -678,10 +691,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
     func testThatKeysCanBeCustomEncoded() {
         // Given
         let encoder = URLEncodedFormEncoder(keyEncoding: .custom { _ in "A" })
-        let paramters = ["oneTwoThree": "oneTwoThree"]
+        let parameters = ["oneTwoThree": "oneTwoThree"]
 
         // When
-        let result = Result<String, Error> { try encoder.encode(paramters) }
+        let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
         XCTAssertEqual(result.success, "A=oneTwoThree")
@@ -717,18 +730,18 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         // Given
         let encoder = URLEncodedFormEncoder()
         let parameters = ["lowercase": "abcdefghijklmnopqrstuvwxyz",
-                          "uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
-                          "numbers": "0123456789"]
+                          "numbers": "0123456789",
+                          "uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
 
         // When
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        XCTAssertQueryEqual(result.success,
-                            "uppercase=ABCDEFGHIJKLMNOPQRSTUVWXYZ&numbers=0123456789&lowercase=abcdefghijklmnopqrstuvwxyz")
+        let expected = "lowercase=abcdefghijklmnopqrstuvwxyz&numbers=0123456789&uppercase=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        XCTAssertEqual(result.success, expected)
     }
 
-    func testThatReseredCharactersArePercentEscaped() {
+    func testThatReservedCharactersArePercentEscaped() {
         // Given
         let encoder = URLEncodedFormEncoder()
         let generalDelimiters = ":#[]@"
@@ -763,7 +776,7 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
         let result = Result<String, Error> { try encoder.encode(parameters) }
 
         // Then
-        XCTAssertQueryEqual(result.success, "foobar=bazqux&foo%26bar=baz%26qux")
+        XCTAssertEqual(result.success, "foo%26bar=baz%26qux&foobar=bazqux")
     }
 
     func testThatQuestionMarksInKeysAndValuesAreNotPercentEscaped() {
@@ -839,10 +852,10 @@ final class URLEncodedFormEncoderTests: BaseTestCase {
 
         // Then
         let expectedParameterValues = ["arabic=%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9",
-                                       "japanese=%E6%97%A5%E6%9C%AC%E8%AA%9E",
+                                       "emoji=%F0%9F%98%83",
                                        "french=fran%C3%A7ais",
-                                       "emoji=%F0%9F%98%83"].joined(separator: "&")
-        XCTAssertQueryEqual(result.success, expectedParameterValues)
+                                       "japanese=%E6%97%A5%E6%9C%AC%E8%AA%9E"].joined(separator: "&")
+        XCTAssertEqual(result.success, expectedParameterValues)
     }
 
     func testStringWithThousandsOfChineseCharactersIsPercentEscaped() {
@@ -979,10 +992,3 @@ private struct FailingOptionalStruct: Encodable {
         }
     }
 }
-
-private func XCTAssertQueryEqual(_ query1: String?, _ query2: String?) {
-    let items1 = query1?.split(separator: "&").sorted()
-    let items2 = query2?.split(separator: "&").sorted()
-
-    XCTAssertEqual(items1, items2)
-}

+ 3 - 3
Tests/RequestTests.swift

@@ -676,7 +676,7 @@ final class RequestResponseTestCase: BaseTestCase {
         var response2: DataResponse<Any, AFError>?
         var response3: DataResponse<Any, AFError>?
 
-        let expect = expectation(description: "both response serializer completions should be called")
+        let expect = expectation(description: "all response serializer completions should be called")
         expect.expectedFulfillmentCount = 3
 
         // When
@@ -855,7 +855,7 @@ final class RequestCURLDescriptionTestCase: BaseTestCase {
         XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
         XCTAssertTrue(components?.contains("-X") == true)
         XCTAssertEqual(components?.last, "\"\(urlString)\"")
-        XCTAssertEqual(components, syncComponents)
+        XCTAssertEqual(components?.sorted(), syncComponents?.sorted())
     }
 
     func testGETRequestCURLDescriptionCanBeRequestedManyTimes() {
@@ -889,7 +889,7 @@ final class RequestCURLDescriptionTestCase: BaseTestCase {
         XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
         XCTAssertTrue(components?.contains("-X") == true)
         XCTAssertEqual(components?.last, "\"\(urlString)\"")
-        XCTAssertEqual(components, secondComponents)
+        XCTAssertEqual(components?.sorted(), secondComponents?.sorted())
     }
 
     func testGETRequestWithCustomHeaderCURLDescription() {