MoyaProvider+Internal.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import Foundation
  2. // MARK: - Method
  3. public extension Method {
  4. /// A Boolean value determining whether the request supports multipart.
  5. var supportsMultipart: Bool {
  6. switch self {
  7. case .post, .put, .patch, .connect:
  8. return true
  9. default:
  10. return false
  11. }
  12. }
  13. }
  14. // MARK: - MoyaProvider
  15. /// Internal extension to keep the inner-workings outside the main Moya.swift file.
  16. public extension MoyaProvider {
  17. /// Performs normal requests.
  18. func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {
  19. let endpoint = self.endpoint(target)
  20. let stubBehavior = self.stubClosure(target)
  21. let cancellableToken = CancellableWrapper()
  22. // Allow plugins to modify response
  23. let pluginsWithCompletion: Moya.Completion = { result in
  24. let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
  25. completion(processedResult)
  26. }
  27. if trackInflights {
  28. lock.lock()
  29. var inflightCompletionBlocks = self.inflightRequests[endpoint]
  30. inflightCompletionBlocks?.append(pluginsWithCompletion)
  31. self.inflightRequests[endpoint] = inflightCompletionBlocks
  32. lock.unlock()
  33. if inflightCompletionBlocks != nil {
  34. return cancellableToken
  35. } else {
  36. lock.lock()
  37. self.inflightRequests[endpoint] = [pluginsWithCompletion]
  38. lock.unlock()
  39. }
  40. }
  41. let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in
  42. if cancellableToken.isCancelled {
  43. self.cancelCompletion(pluginsWithCompletion, target: target)
  44. return
  45. }
  46. var request: URLRequest!
  47. switch requestResult {
  48. case .success(let urlRequest):
  49. request = urlRequest
  50. case .failure(let error):
  51. pluginsWithCompletion(.failure(error))
  52. return
  53. }
  54. let networkCompletion: Moya.Completion = { result in
  55. if self.trackInflights {
  56. self.inflightRequests[endpoint]?.forEach { $0(result) }
  57. self.lock.lock()
  58. self.inflightRequests.removeValue(forKey: endpoint)
  59. self.lock.unlock()
  60. } else {
  61. pluginsWithCompletion(result)
  62. }
  63. }
  64. cancellableToken.innerCancellable = self.performRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
  65. }
  66. requestClosure(endpoint, performNetworking)
  67. return cancellableToken
  68. }
  69. // swiftlint:disable:next function_parameter_count
  70. private func performRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion, endpoint: Endpoint, stubBehavior: Moya.StubBehavior) -> Cancellable {
  71. switch stubBehavior {
  72. case .never:
  73. switch endpoint.task {
  74. case .requestPlain, .requestData, .requestJSONEncodable, .requestCustomJSONEncodable, .requestParameters, .requestCompositeData, .requestCompositeParameters:
  75. return self.sendRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: completion)
  76. case .uploadFile(let file):
  77. return self.sendUploadFile(target, request: request, callbackQueue: callbackQueue, file: file, progress: progress, completion: completion)
  78. case .uploadMultipart(let multipartBody), .uploadCompositeMultipart(let multipartBody, _):
  79. guard !multipartBody.isEmpty && endpoint.method.supportsMultipart else {
  80. fatalError("\(target) is not a multipart upload target.")
  81. }
  82. return self.sendUploadMultipart(target, request: request, callbackQueue: callbackQueue, multipartBody: multipartBody, progress: progress, completion: completion)
  83. case .downloadDestination(let destination), .downloadParameters(_, _, let destination):
  84. return self.sendDownloadRequest(target, request: request, callbackQueue: callbackQueue, destination: destination, progress: progress, completion: completion)
  85. }
  86. default:
  87. return self.stubRequest(target, request: request, callbackQueue: callbackQueue, completion: completion, endpoint: endpoint, stubBehavior: stubBehavior)
  88. }
  89. }
  90. func cancelCompletion(_ completion: Moya.Completion, target: Target) {
  91. let error = MoyaError.underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil), nil)
  92. plugins.forEach { $0.didReceive(.failure(error), target: target) }
  93. completion(.failure(error))
  94. }
  95. /// Creates a function which, when called, executes the appropriate stubbing behavior for the given parameters.
  96. final func createStubFunction(_ token: CancellableToken, forTarget target: Target, withCompletion completion: @escaping Moya.Completion, endpoint: Endpoint, plugins: [PluginType], request: URLRequest) -> (() -> Void) { // swiftlint:disable:this function_parameter_count
  97. return {
  98. if token.isCancelled {
  99. self.cancelCompletion(completion, target: target)
  100. return
  101. }
  102. let validate = { (response: Moya.Response) -> Result<Moya.Response, MoyaError> in
  103. let validCodes = target.validationType.statusCodes
  104. guard !validCodes.isEmpty else { return .success(response) }
  105. if validCodes.contains(response.statusCode) {
  106. return .success(response)
  107. } else {
  108. let statusError = MoyaError.statusCode(response)
  109. let error = MoyaError.underlying(statusError, response)
  110. return .failure(error)
  111. }
  112. }
  113. switch endpoint.sampleResponseClosure() {
  114. case .networkResponse(let statusCode, let data):
  115. let response = Moya.Response(statusCode: statusCode, data: data, request: request, response: nil)
  116. let result = validate(response)
  117. plugins.forEach { $0.didReceive(result, target: target) }
  118. completion(result)
  119. case .response(let customResponse, let data):
  120. let response = Moya.Response(statusCode: customResponse.statusCode, data: data, request: request, response: customResponse)
  121. let result = validate(response)
  122. plugins.forEach { $0.didReceive(result, target: target) }
  123. completion(result)
  124. case .networkError(let error):
  125. let error = MoyaError.underlying(error, nil)
  126. plugins.forEach { $0.didReceive(.failure(error), target: target) }
  127. completion(.failure(error))
  128. }
  129. }
  130. }
  131. /// Notify all plugins that a stub is about to be performed. You must call this if overriding `stubRequest`.
  132. final func notifyPluginsOfImpendingStub(for request: URLRequest, target: Target) -> URLRequest {
  133. let alamoRequest = session.request(request)
  134. alamoRequest.cancel()
  135. let preparedRequest = plugins.reduce(request) { $1.prepare($0, target: target) }
  136. let stubbedAlamoRequest = RequestTypeWrapper(request: alamoRequest, urlRequest: preparedRequest)
  137. plugins.forEach { $0.willSend(stubbedAlamoRequest, target: target) }
  138. return preparedRequest
  139. }
  140. }
  141. private extension MoyaProvider {
  142. private func interceptor(target: Target) -> MoyaRequestInterceptor {
  143. return MoyaRequestInterceptor(prepare: { [weak self] urlRequest in
  144. return self?.plugins.reduce(urlRequest) { $1.prepare($0, target: target) } ?? urlRequest
  145. })
  146. }
  147. private func setup(interceptor: MoyaRequestInterceptor, with target: Target, and request: Request) {
  148. interceptor.willSend = { [weak self, weak request] urlRequest in
  149. guard let self = self, let request = request else { return }
  150. let stubbedAlamoRequest = RequestTypeWrapper(request: request, urlRequest: urlRequest)
  151. self.plugins.forEach { $0.willSend(stubbedAlamoRequest, target: target) }
  152. }
  153. }
  154. func sendUploadMultipart(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, multipartBody: [MultipartFormData], progress: Moya.ProgressBlock? = nil, completion: @escaping Moya.Completion) -> CancellableToken {
  155. let formData = RequestMultipartFormData()
  156. formData.applyMoyaMultipartFormData(multipartBody)
  157. let interceptor = self.interceptor(target: target)
  158. let request = session.upload(multipartFormData: formData, with: request, interceptor: interceptor)
  159. setup(interceptor: interceptor, with: target, and: request)
  160. let validationCodes = target.validationType.statusCodes
  161. let validatedRequest = validationCodes.isEmpty ? request : request.validate(statusCode: validationCodes)
  162. return sendAlamofireRequest(validatedRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
  163. }
  164. func sendUploadFile(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, file: URL, progress: ProgressBlock? = nil, completion: @escaping Completion) -> CancellableToken {
  165. let interceptor = self.interceptor(target: target)
  166. let uploadRequest = session.upload(file, with: request, interceptor: interceptor)
  167. setup(interceptor: interceptor, with: target, and: uploadRequest)
  168. let validationCodes = target.validationType.statusCodes
  169. let alamoRequest = validationCodes.isEmpty ? uploadRequest : uploadRequest.validate(statusCode: validationCodes)
  170. return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
  171. }
  172. func sendDownloadRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, destination: @escaping DownloadDestination, progress: ProgressBlock? = nil, completion: @escaping Completion) -> CancellableToken {
  173. let interceptor = self.interceptor(target: target)
  174. let downloadRequest = session.download(request, interceptor: interceptor, to: destination)
  175. setup(interceptor: interceptor, with: target, and: downloadRequest)
  176. let validationCodes = target.validationType.statusCodes
  177. let alamoRequest = validationCodes.isEmpty ? downloadRequest : downloadRequest.validate(statusCode: validationCodes)
  178. return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
  179. }
  180. func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken {
  181. let interceptor = self.interceptor(target: target)
  182. let initialRequest = session.request(request, interceptor: interceptor)
  183. setup(interceptor: interceptor, with: target, and: initialRequest)
  184. let validationCodes = target.validationType.statusCodes
  185. let alamoRequest = validationCodes.isEmpty ? initialRequest : initialRequest.validate(statusCode: validationCodes)
  186. return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
  187. }
  188. // swiftlint:disable:next cyclomatic_complexity
  189. func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request {
  190. // Give plugins the chance to alter the outgoing request
  191. let plugins = self.plugins
  192. var progressAlamoRequest = alamoRequest
  193. let progressClosure: (Progress) -> Void = { progress in
  194. let sendProgress: () -> Void = {
  195. progressCompletion?(ProgressResponse(progress: progress))
  196. }
  197. if let callbackQueue = callbackQueue {
  198. callbackQueue.async(execute: sendProgress)
  199. } else {
  200. sendProgress()
  201. }
  202. }
  203. // Perform the actual request
  204. if progressCompletion != nil {
  205. switch progressAlamoRequest {
  206. case let downloadRequest as DownloadRequest:
  207. if let downloadRequest = downloadRequest.downloadProgress(closure: progressClosure) as? T {
  208. progressAlamoRequest = downloadRequest
  209. }
  210. case let uploadRequest as UploadRequest:
  211. if let uploadRequest = uploadRequest.uploadProgress(closure: progressClosure) as? T {
  212. progressAlamoRequest = uploadRequest
  213. }
  214. case let dataRequest as DataRequest:
  215. if let dataRequest = dataRequest.downloadProgress(closure: progressClosure) as? T {
  216. progressAlamoRequest = dataRequest
  217. }
  218. default: break
  219. }
  220. }
  221. let completionHandler: RequestableCompletion = { response, request, data, error in
  222. let result = convertResponseToResult(response, request: request, data: data, error: error)
  223. // Inform all plugins about the response
  224. plugins.forEach { $0.didReceive(result, target: target) }
  225. if let progressCompletion = progressCompletion {
  226. let value = try? result.get()
  227. switch progressAlamoRequest {
  228. case let downloadRequest as DownloadRequest:
  229. progressCompletion(ProgressResponse(progress: downloadRequest.downloadProgress, response: value))
  230. case let uploadRequest as UploadRequest:
  231. progressCompletion(ProgressResponse(progress: uploadRequest.uploadProgress, response: value))
  232. case let dataRequest as DataRequest:
  233. progressCompletion(ProgressResponse(progress: dataRequest.downloadProgress, response: value))
  234. default:
  235. progressCompletion(ProgressResponse(response: value))
  236. }
  237. }
  238. completion(result)
  239. }
  240. progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler)
  241. progressAlamoRequest.resume()
  242. return CancellableToken(request: progressAlamoRequest)
  243. }
  244. }