1 |
efrain |
1 |
//
|
|
|
2 |
// ParameterEncoding.swift
|
|
|
3 |
//
|
|
|
4 |
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
|
|
5 |
//
|
|
|
6 |
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
7 |
// of this software and associated documentation files (the "Software"), to deal
|
|
|
8 |
// in the Software without restriction, including without limitation the rights
|
|
|
9 |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
10 |
// copies of the Software, and to permit persons to whom the Software is
|
|
|
11 |
// furnished to do so, subject to the following conditions:
|
|
|
12 |
//
|
|
|
13 |
// The above copyright notice and this permission notice shall be included in
|
|
|
14 |
// all copies or substantial portions of the Software.
|
|
|
15 |
//
|
|
|
16 |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
17 |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
18 |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
19 |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
20 |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
21 |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
22 |
// THE SOFTWARE.
|
|
|
23 |
//
|
|
|
24 |
|
|
|
25 |
import Foundation
|
|
|
26 |
|
|
|
27 |
/// A dictionary of parameters to apply to a `URLRequest`.
|
|
|
28 |
public typealias Parameters = [String: Any]
|
|
|
29 |
|
|
|
30 |
/// A type used to define how a set of parameters are applied to a `URLRequest`.
|
|
|
31 |
public protocol ParameterEncoding {
|
|
|
32 |
/// Creates a `URLRequest` by encoding parameters and applying them on the passed request.
|
|
|
33 |
///
|
|
|
34 |
/// - Parameters:
|
|
|
35 |
/// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded.
|
|
|
36 |
/// - parameters: `Parameters` to encode onto the request.
|
|
|
37 |
///
|
|
|
38 |
/// - Returns: The encoded `URLRequest`.
|
|
|
39 |
/// - Throws: Any `Error` produced during parameter encoding.
|
|
|
40 |
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
|
|
|
41 |
}
|
|
|
42 |
|
|
|
43 |
// MARK: -
|
|
|
44 |
|
|
|
45 |
/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
|
|
|
46 |
/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
|
|
|
47 |
/// the HTTP body depends on the destination of the encoding.
|
|
|
48 |
///
|
|
|
49 |
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
|
|
|
50 |
/// `application/x-www-form-urlencoded; charset=utf-8`.
|
|
|
51 |
///
|
|
|
52 |
/// There is no published specification for how to encode collection types. By default the convention of appending
|
|
|
53 |
/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
|
|
|
54 |
/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
|
|
|
55 |
/// square brackets appended to array keys.
|
|
|
56 |
///
|
|
|
57 |
/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
|
|
|
58 |
/// `true` as 1 and `false` as 0.
|
|
|
59 |
public struct URLEncoding: ParameterEncoding {
|
|
|
60 |
// MARK: Helper Types
|
|
|
61 |
|
|
|
62 |
/// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
|
|
|
63 |
/// resulting URL request.
|
|
|
64 |
public enum Destination {
|
|
|
65 |
/// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and
|
|
|
66 |
/// sets as the HTTP body for requests with any other HTTP method.
|
|
|
67 |
case methodDependent
|
|
|
68 |
/// Sets or appends encoded query string result to existing query string.
|
|
|
69 |
case queryString
|
|
|
70 |
/// Sets encoded query string result as the HTTP body of the URL request.
|
|
|
71 |
case httpBody
|
|
|
72 |
|
|
|
73 |
func encodesParametersInURL(for method: HTTPMethod) -> Bool {
|
|
|
74 |
switch self {
|
|
|
75 |
case .methodDependent: return [.get, .head, .delete].contains(method)
|
|
|
76 |
case .queryString: return true
|
|
|
77 |
case .httpBody: return false
|
|
|
78 |
}
|
|
|
79 |
}
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
/// Configures how `Array` parameters are encoded.
|
|
|
83 |
public enum ArrayEncoding {
|
|
|
84 |
/// An empty set of square brackets is appended to the key for every value. This is the default behavior.
|
|
|
85 |
case brackets
|
|
|
86 |
/// No brackets are appended. The key is encoded as is.
|
|
|
87 |
case noBrackets
|
|
|
88 |
/// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior.
|
|
|
89 |
case indexInBrackets
|
|
|
90 |
|
|
|
91 |
func encode(key: String, atIndex index: Int) -> String {
|
|
|
92 |
switch self {
|
|
|
93 |
case .brackets:
|
|
|
94 |
return "\(key)[]"
|
|
|
95 |
case .noBrackets:
|
|
|
96 |
return key
|
|
|
97 |
case .indexInBrackets:
|
|
|
98 |
return "\(key)[\(index)]"
|
|
|
99 |
}
|
|
|
100 |
}
|
|
|
101 |
}
|
|
|
102 |
|
|
|
103 |
/// Configures how `Bool` parameters are encoded.
|
|
|
104 |
public enum BoolEncoding {
|
|
|
105 |
/// Encode `true` as `1` and `false` as `0`. This is the default behavior.
|
|
|
106 |
case numeric
|
|
|
107 |
/// Encode `true` and `false` as string literals.
|
|
|
108 |
case literal
|
|
|
109 |
|
|
|
110 |
func encode(value: Bool) -> String {
|
|
|
111 |
switch self {
|
|
|
112 |
case .numeric:
|
|
|
113 |
return value ? "1" : "0"
|
|
|
114 |
case .literal:
|
|
|
115 |
return value ? "true" : "false"
|
|
|
116 |
}
|
|
|
117 |
}
|
|
|
118 |
}
|
|
|
119 |
|
|
|
120 |
// MARK: Properties
|
|
|
121 |
|
|
|
122 |
/// Returns a default `URLEncoding` instance with a `.methodDependent` destination.
|
|
|
123 |
public static var `default`: URLEncoding { URLEncoding() }
|
|
|
124 |
|
|
|
125 |
/// Returns a `URLEncoding` instance with a `.queryString` destination.
|
|
|
126 |
public static var queryString: URLEncoding { URLEncoding(destination: .queryString) }
|
|
|
127 |
|
|
|
128 |
/// Returns a `URLEncoding` instance with an `.httpBody` destination.
|
|
|
129 |
public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) }
|
|
|
130 |
|
|
|
131 |
/// The destination defining where the encoded query string is to be applied to the URL request.
|
|
|
132 |
public let destination: Destination
|
|
|
133 |
|
|
|
134 |
/// The encoding to use for `Array` parameters.
|
|
|
135 |
public let arrayEncoding: ArrayEncoding
|
|
|
136 |
|
|
|
137 |
/// The encoding to use for `Bool` parameters.
|
|
|
138 |
public let boolEncoding: BoolEncoding
|
|
|
139 |
|
|
|
140 |
// MARK: Initialization
|
|
|
141 |
|
|
|
142 |
/// Creates an instance using the specified parameters.
|
|
|
143 |
///
|
|
|
144 |
/// - Parameters:
|
|
|
145 |
/// - destination: `Destination` defining where the encoded query string will be applied. `.methodDependent` by
|
|
|
146 |
/// default.
|
|
|
147 |
/// - arrayEncoding: `ArrayEncoding` to use. `.brackets` by default.
|
|
|
148 |
/// - boolEncoding: `BoolEncoding` to use. `.numeric` by default.
|
|
|
149 |
public init(destination: Destination = .methodDependent,
|
|
|
150 |
arrayEncoding: ArrayEncoding = .brackets,
|
|
|
151 |
boolEncoding: BoolEncoding = .numeric) {
|
|
|
152 |
self.destination = destination
|
|
|
153 |
self.arrayEncoding = arrayEncoding
|
|
|
154 |
self.boolEncoding = boolEncoding
|
|
|
155 |
}
|
|
|
156 |
|
|
|
157 |
// MARK: Encoding
|
|
|
158 |
|
|
|
159 |
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
|
|
|
160 |
var urlRequest = try urlRequest.asURLRequest()
|
|
|
161 |
|
|
|
162 |
guard let parameters = parameters else { return urlRequest }
|
|
|
163 |
|
|
|
164 |
if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
|
|
|
165 |
guard let url = urlRequest.url else {
|
|
|
166 |
throw AFError.parameterEncodingFailed(reason: .missingURL)
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
|
|
|
170 |
let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
|
|
|
171 |
urlComponents.percentEncodedQuery = percentEncodedQuery
|
|
|
172 |
urlRequest.url = urlComponents.url
|
|
|
173 |
}
|
|
|
174 |
} else {
|
|
|
175 |
if urlRequest.headers["Content-Type"] == nil {
|
|
|
176 |
urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
|
|
|
177 |
}
|
|
|
178 |
|
|
|
179 |
urlRequest.httpBody = Data(query(parameters).utf8)
|
|
|
180 |
}
|
|
|
181 |
|
|
|
182 |
return urlRequest
|
|
|
183 |
}
|
|
|
184 |
|
|
|
185 |
/// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively.
|
|
|
186 |
///
|
|
|
187 |
/// - Parameters:
|
|
|
188 |
/// - key: Key of the query component.
|
|
|
189 |
/// - value: Value of the query component.
|
|
|
190 |
///
|
|
|
191 |
/// - Returns: The percent-escaped, URL encoded query string components.
|
|
|
192 |
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
|
|
|
193 |
var components: [(String, String)] = []
|
|
|
194 |
switch value {
|
|
|
195 |
case let dictionary as [String: Any]:
|
|
|
196 |
for (nestedKey, value) in dictionary {
|
|
|
197 |
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
|
|
|
198 |
}
|
|
|
199 |
case let array as [Any]:
|
|
|
200 |
for (index, value) in array.enumerated() {
|
|
|
201 |
components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value)
|
|
|
202 |
}
|
|
|
203 |
case let number as NSNumber:
|
|
|
204 |
if number.isBool {
|
|
|
205 |
components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
|
|
|
206 |
} else {
|
|
|
207 |
components.append((escape(key), escape("\(number)")))
|
|
|
208 |
}
|
|
|
209 |
case let bool as Bool:
|
|
|
210 |
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
|
|
|
211 |
default:
|
|
|
212 |
components.append((escape(key), escape("\(value)")))
|
|
|
213 |
}
|
|
|
214 |
return components
|
|
|
215 |
}
|
|
|
216 |
|
|
|
217 |
/// Creates a percent-escaped string following RFC 3986 for a query string key or value.
|
|
|
218 |
///
|
|
|
219 |
/// - Parameter string: `String` to be percent-escaped.
|
|
|
220 |
///
|
|
|
221 |
/// - Returns: The percent-escaped `String`.
|
|
|
222 |
public func escape(_ string: String) -> String {
|
|
|
223 |
string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
|
|
|
224 |
}
|
|
|
225 |
|
|
|
226 |
private func query(_ parameters: [String: Any]) -> String {
|
|
|
227 |
var components: [(String, String)] = []
|
|
|
228 |
|
|
|
229 |
for key in parameters.keys.sorted(by: <) {
|
|
|
230 |
let value = parameters[key]!
|
|
|
231 |
components += queryComponents(fromKey: key, value: value)
|
|
|
232 |
}
|
|
|
233 |
return components.map { "\($0)=\($1)" }.joined(separator: "&")
|
|
|
234 |
}
|
|
|
235 |
}
|
|
|
236 |
|
|
|
237 |
// MARK: -
|
|
|
238 |
|
|
|
239 |
/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
|
|
|
240 |
/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
|
|
|
241 |
public struct JSONEncoding: ParameterEncoding {
|
|
|
242 |
// MARK: Properties
|
|
|
243 |
|
|
|
244 |
/// Returns a `JSONEncoding` instance with default writing options.
|
|
|
245 |
public static var `default`: JSONEncoding { JSONEncoding() }
|
|
|
246 |
|
|
|
247 |
/// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
|
|
|
248 |
public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) }
|
|
|
249 |
|
|
|
250 |
/// The options for writing the parameters as JSON data.
|
|
|
251 |
public let options: JSONSerialization.WritingOptions
|
|
|
252 |
|
|
|
253 |
// MARK: Initialization
|
|
|
254 |
|
|
|
255 |
/// Creates an instance using the specified `WritingOptions`.
|
|
|
256 |
///
|
|
|
257 |
/// - Parameter options: `JSONSerialization.WritingOptions` to use.
|
|
|
258 |
public init(options: JSONSerialization.WritingOptions = []) {
|
|
|
259 |
self.options = options
|
|
|
260 |
}
|
|
|
261 |
|
|
|
262 |
// MARK: Encoding
|
|
|
263 |
|
|
|
264 |
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
|
|
|
265 |
var urlRequest = try urlRequest.asURLRequest()
|
|
|
266 |
|
|
|
267 |
guard let parameters = parameters else { return urlRequest }
|
|
|
268 |
|
|
|
269 |
do {
|
|
|
270 |
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
|
|
|
271 |
|
|
|
272 |
if urlRequest.headers["Content-Type"] == nil {
|
|
|
273 |
urlRequest.headers.update(.contentType("application/json"))
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
urlRequest.httpBody = data
|
|
|
277 |
} catch {
|
|
|
278 |
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
return urlRequest
|
|
|
282 |
}
|
|
|
283 |
|
|
|
284 |
/// Encodes any JSON compatible object into a `URLRequest`.
|
|
|
285 |
///
|
|
|
286 |
/// - Parameters:
|
|
|
287 |
/// - urlRequest: `URLRequestConvertible` value into which the object will be encoded.
|
|
|
288 |
/// - jsonObject: `Any` value (must be JSON compatible` to be encoded into the `URLRequest`. `nil` by default.
|
|
|
289 |
///
|
|
|
290 |
/// - Returns: The encoded `URLRequest`.
|
|
|
291 |
/// - Throws: Any `Error` produced during encoding.
|
|
|
292 |
public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
|
|
|
293 |
var urlRequest = try urlRequest.asURLRequest()
|
|
|
294 |
|
|
|
295 |
guard let jsonObject = jsonObject else { return urlRequest }
|
|
|
296 |
|
|
|
297 |
do {
|
|
|
298 |
let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
|
|
|
299 |
|
|
|
300 |
if urlRequest.headers["Content-Type"] == nil {
|
|
|
301 |
urlRequest.headers.update(.contentType("application/json"))
|
|
|
302 |
}
|
|
|
303 |
|
|
|
304 |
urlRequest.httpBody = data
|
|
|
305 |
} catch {
|
|
|
306 |
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
|
|
|
307 |
}
|
|
|
308 |
|
|
|
309 |
return urlRequest
|
|
|
310 |
}
|
|
|
311 |
}
|
|
|
312 |
|
|
|
313 |
// MARK: -
|
|
|
314 |
|
|
|
315 |
extension NSNumber {
|
|
|
316 |
fileprivate var isBool: Bool {
|
|
|
317 |
// Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of
|
|
|
318 |
// swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22).
|
|
|
319 |
String(cString: objCType) == "c"
|
|
|
320 |
}
|
|
|
321 |
}
|