Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions

874
Pods/Alamofire/Source/Core/AFError.swift generated Normal file
View File

@ -0,0 +1,874 @@
//
// AFError.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
#if canImport(Security)
import Security
#endif
/// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with
/// their own associated reasons.
public enum AFError: Error {
/// The underlying reason the `.multipartEncodingFailed` error occurred.
public enum MultipartEncodingFailureReason {
/// The `fileURL` provided for reading an encodable body part isn't a file `URL`.
case bodyPartURLInvalid(url: URL)
/// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension`.
case bodyPartFilenameInvalid(in: URL)
/// The file at the `fileURL` provided was not reachable.
case bodyPartFileNotReachable(at: URL)
/// Attempting to check the reachability of the `fileURL` provided threw an error.
case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
/// The file at the `fileURL` provided is actually a directory.
case bodyPartFileIsDirectory(at: URL)
/// The size of the file at the `fileURL` provided was not returned by the system.
case bodyPartFileSizeNotAvailable(at: URL)
/// The attempt to find the size of the file at the `fileURL` provided threw an error.
case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
/// An `InputStream` could not be created for the provided `fileURL`.
case bodyPartInputStreamCreationFailed(for: URL)
/// An `OutputStream` could not be created when attempting to write the encoded data to disk.
case outputStreamCreationFailed(for: URL)
/// The encoded body data could not be written to disk because a file already exists at the provided `fileURL`.
case outputStreamFileAlreadyExists(at: URL)
/// The `fileURL` provided for writing the encoded body data to disk is not a file `URL`.
case outputStreamURLInvalid(url: URL)
/// The attempt to write the encoded body data to disk failed with an underlying error.
case outputStreamWriteFailed(error: Error)
/// The attempt to read an encoded body part `InputStream` failed with underlying system error.
case inputStreamReadFailed(error: Error)
}
/// Represents unexpected input stream length that occur when encoding the `MultipartFormData`. Instances will be
/// embedded within an `AFError.multipartEncodingFailed` `.inputStreamReadFailed` case.
public struct UnexpectedInputStreamLength: Error {
/// The expected byte count to read.
public var bytesExpected: UInt64
/// The actual byte count read.
public var bytesRead: UInt64
}
/// The underlying reason the `.parameterEncodingFailed` error occurred.
public enum ParameterEncodingFailureReason {
/// The `URLRequest` did not have a `URL` to encode.
case missingURL
/// JSON serialization failed with an underlying system error during the encoding process.
case jsonEncodingFailed(error: Error)
/// Custom parameter encoding failed due to the associated `Error`.
case customEncodingFailed(error: Error)
}
/// The underlying reason the `.parameterEncoderFailed` error occurred.
public enum ParameterEncoderFailureReason {
/// Possible missing components.
public enum RequiredComponent {
/// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding.
case url
/// The `HTTPMethod` could not be extracted from the passed `URLRequest`.
case httpMethod(rawValue: String)
}
/// A `RequiredComponent` was missing during encoding.
case missingRequiredComponent(RequiredComponent)
/// The underlying encoder failed with the associated error.
case encoderFailed(error: Error)
}
/// The underlying reason the `.responseValidationFailed` error occurred.
public enum ResponseValidationFailureReason {
/// The data file containing the server response did not exist.
case dataFileNil
/// The data file containing the server response at the associated `URL` could not be read.
case dataFileReadFailed(at: URL)
/// The response did not contain a `Content-Type` and the `acceptableContentTypes` provided did not contain a
/// wildcard type.
case missingContentType(acceptableContentTypes: [String])
/// The response `Content-Type` did not match any type in the provided `acceptableContentTypes`.
case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
/// The response status code was not acceptable.
case unacceptableStatusCode(code: Int)
/// Custom response validation failed due to the associated `Error`.
case customValidationFailed(error: Error)
}
/// The underlying reason the response serialization error occurred.
public enum ResponseSerializationFailureReason {
/// The server response contained no data or the data was zero length.
case inputDataNilOrZeroLength
/// The file containing the server response did not exist.
case inputFileNil
/// The file containing the server response could not be read from the associated `URL`.
case inputFileReadFailed(at: URL)
/// String serialization failed using the provided `String.Encoding`.
case stringSerializationFailed(encoding: String.Encoding)
/// JSON serialization failed with an underlying system error.
case jsonSerializationFailed(error: Error)
/// A `DataDecoder` failed to decode the response due to the associated `Error`.
case decodingFailed(error: Error)
/// A custom response serializer failed due to the associated `Error`.
case customSerializationFailed(error: Error)
/// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type.
case invalidEmptyResponse(type: String)
}
#if canImport(Security)
/// Underlying reason a server trust evaluation error occurred.
public enum ServerTrustFailureReason {
/// The output of a server trust evaluation.
public struct Output {
/// The host for which the evaluation was performed.
public let host: String
/// The `SecTrust` value which was evaluated.
public let trust: SecTrust
/// The `OSStatus` of evaluation operation.
public let status: OSStatus
/// The result of the evaluation operation.
public let result: SecTrustResultType
/// Creates an `Output` value from the provided values.
init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) {
self.host = host
self.trust = trust
self.status = status
self.result = result
}
}
/// No `ServerTrustEvaluator` was found for the associated host.
case noRequiredEvaluator(host: String)
/// No certificates were found with which to perform the trust evaluation.
case noCertificatesFound
/// No public keys were found with which to perform the trust evaluation.
case noPublicKeysFound
/// During evaluation, application of the associated `SecPolicy` failed.
case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus)
/// During evaluation, setting the associated anchor certificates failed.
case settingAnchorCertificatesFailed(status: OSStatus, certificates: [SecCertificate])
/// During evaluation, creation of the revocation policy failed.
case revocationPolicyCreationFailed
/// `SecTrust` evaluation failed with the associated `Error`, if one was produced.
case trustEvaluationFailed(error: Error?)
/// Default evaluation failed with the associated `Output`.
case defaultEvaluationFailed(output: Output)
/// Host validation failed with the associated `Output`.
case hostValidationFailed(output: Output)
/// Revocation check failed with the associated `Output` and options.
case revocationCheckFailed(output: Output, options: RevocationTrustEvaluator.Options)
/// Certificate pinning failed.
case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate])
/// Public key pinning failed.
case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey])
/// Custom server trust evaluation failed due to the associated `Error`.
case customEvaluationFailed(error: Error)
}
#endif
/// The underlying reason the `.urlRequestValidationFailed` error occurred.
public enum URLRequestValidationFailureReason {
/// URLRequest with GET method had body data.
case bodyDataInGETRequest(Data)
}
/// `UploadableConvertible` threw an error in `createUploadable()`.
case createUploadableFailed(error: Error)
/// `URLRequestConvertible` threw an error in `asURLRequest()`.
case createURLRequestFailed(error: Error)
/// `SessionDelegate` threw an error while attempting to move downloaded file to destination URL.
case downloadedFileMoveFailed(error: Error, source: URL, destination: URL)
/// `Request` was explicitly cancelled.
case explicitlyCancelled
/// `URLConvertible` type failed to create a valid `URL`.
case invalidURL(url: URLConvertible)
/// Multipart form encoding failed.
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
/// `ParameterEncoding` threw an error during the encoding process.
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
/// `ParameterEncoder` threw an error while running the encoder.
case parameterEncoderFailed(reason: ParameterEncoderFailureReason)
/// `RequestAdapter` threw an error during adaptation.
case requestAdaptationFailed(error: Error)
/// `RequestRetrier` threw an error during the request retry process.
case requestRetryFailed(retryError: Error, originalError: Error)
/// Response validation failed.
case responseValidationFailed(reason: ResponseValidationFailureReason)
/// Response serialization failed.
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
#if canImport(Security)
/// `ServerTrustEvaluating` instance threw an error during trust evaluation.
case serverTrustEvaluationFailed(reason: ServerTrustFailureReason)
#endif
/// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope.
case sessionDeinitialized
/// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`.
case sessionInvalidated(error: Error?)
/// `URLSessionTask` completed with error.
case sessionTaskFailed(error: Error)
/// `URLRequest` failed validation.
case urlRequestValidationFailed(reason: URLRequestValidationFailureReason)
}
extension Error {
/// Returns the instance cast as an `AFError`.
public var asAFError: AFError? {
self as? AFError
}
/// Returns the instance cast as an `AFError`. If casting fails, a `fatalError` with the specified `message` is thrown.
public func asAFError(orFailWith message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> AFError {
guard let afError = self as? AFError else {
fatalError(message(), file: file, line: line)
}
return afError
}
/// Casts the instance as `AFError` or returns `defaultAFError`
func asAFError(or defaultAFError: @autoclosure () -> AFError) -> AFError {
self as? AFError ?? defaultAFError()
}
}
// MARK: - Error Booleans
extension AFError {
/// Returns whether the instance is `.sessionDeinitialized`.
public var isSessionDeinitializedError: Bool {
if case .sessionDeinitialized = self { return true }
return false
}
/// Returns whether the instance is `.sessionInvalidated`.
public var isSessionInvalidatedError: Bool {
if case .sessionInvalidated = self { return true }
return false
}
/// Returns whether the instance is `.explicitlyCancelled`.
public var isExplicitlyCancelledError: Bool {
if case .explicitlyCancelled = self { return true }
return false
}
/// Returns whether the instance is `.invalidURL`.
public var isInvalidURLError: Bool {
if case .invalidURL = self { return true }
return false
}
/// Returns whether the instance is `.parameterEncodingFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isParameterEncodingError: Bool {
if case .parameterEncodingFailed = self { return true }
return false
}
/// Returns whether the instance is `.parameterEncoderFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isParameterEncoderError: Bool {
if case .parameterEncoderFailed = self { return true }
return false
}
/// Returns whether the instance is `.multipartEncodingFailed`. When `true`, the `url` and `underlyingError`
/// properties will contain the associated values.
public var isMultipartEncodingError: Bool {
if case .multipartEncodingFailed = self { return true }
return false
}
/// Returns whether the instance is `.requestAdaptationFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isRequestAdaptationError: Bool {
if case .requestAdaptationFailed = self { return true }
return false
}
/// Returns whether the instance is `.responseValidationFailed`. When `true`, the `acceptableContentTypes`,
/// `responseContentType`, `responseCode`, and `underlyingError` properties will contain the associated values.
public var isResponseValidationError: Bool {
if case .responseValidationFailed = self { return true }
return false
}
/// Returns whether the instance is `.responseSerializationFailed`. When `true`, the `failedStringEncoding` and
/// `underlyingError` properties will contain the associated values.
public var isResponseSerializationError: Bool {
if case .responseSerializationFailed = self { return true }
return false
}
#if canImport(Security)
/// Returns whether the instance is `.serverTrustEvaluationFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isServerTrustEvaluationError: Bool {
if case .serverTrustEvaluationFailed = self { return true }
return false
}
#endif
/// Returns whether the instance is `requestRetryFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isRequestRetryError: Bool {
if case .requestRetryFailed = self { return true }
return false
}
/// Returns whether the instance is `createUploadableFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isCreateUploadableError: Bool {
if case .createUploadableFailed = self { return true }
return false
}
/// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isCreateURLRequestError: Bool {
if case .createURLRequestFailed = self { return true }
return false
}
/// Returns whether the instance is `downloadedFileMoveFailed`. When `true`, the `destination` and `underlyingError` properties will
/// contain the associated values.
public var isDownloadedFileMoveError: Bool {
if case .downloadedFileMoveFailed = self { return true }
return false
}
/// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isSessionTaskError: Bool {
if case .sessionTaskFailed = self { return true }
return false
}
}
// MARK: - Convenience Properties
extension AFError {
/// The `URLConvertible` associated with the error.
public var urlConvertible: URLConvertible? {
guard case let .invalidURL(url) = self else { return nil }
return url
}
/// The `URL` associated with the error.
public var url: URL? {
guard case let .multipartEncodingFailed(reason) = self else { return nil }
return reason.url
}
/// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`,
/// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`,
/// `.responseSerializationFailed`, `.requestRetryFailed` errors.
public var underlyingError: Error? {
switch self {
case let .multipartEncodingFailed(reason):
return reason.underlyingError
case let .parameterEncodingFailed(reason):
return reason.underlyingError
case let .parameterEncoderFailed(reason):
return reason.underlyingError
case let .requestAdaptationFailed(error):
return error
case let .requestRetryFailed(retryError, _):
return retryError
case let .responseValidationFailed(reason):
return reason.underlyingError
case let .responseSerializationFailed(reason):
return reason.underlyingError
#if canImport(Security)
case let .serverTrustEvaluationFailed(reason):
return reason.underlyingError
#endif
case let .sessionInvalidated(error):
return error
case let .createUploadableFailed(error):
return error
case let .createURLRequestFailed(error):
return error
case let .downloadedFileMoveFailed(error, _, _):
return error
case let .sessionTaskFailed(error):
return error
case .explicitlyCancelled,
.invalidURL,
.sessionDeinitialized,
.urlRequestValidationFailed:
return nil
}
}
/// The acceptable `Content-Type`s of a `.responseValidationFailed` error.
public var acceptableContentTypes: [String]? {
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.acceptableContentTypes
}
/// The response `Content-Type` of a `.responseValidationFailed` error.
public var responseContentType: String? {
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.responseContentType
}
/// The response code of a `.responseValidationFailed` error.
public var responseCode: Int? {
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.responseCode
}
/// The `String.Encoding` associated with a failed `.stringResponse()` call.
public var failedStringEncoding: String.Encoding? {
guard case let .responseSerializationFailed(reason) = self else { return nil }
return reason.failedStringEncoding
}
/// The `source` URL of a `.downloadedFileMoveFailed` error.
public var sourceURL: URL? {
guard case let .downloadedFileMoveFailed(_, source, _) = self else { return nil }
return source
}
/// The `destination` URL of a `.downloadedFileMoveFailed` error.
public var destinationURL: URL? {
guard case let .downloadedFileMoveFailed(_, _, destination) = self else { return nil }
return destination
}
#if canImport(Security)
/// The download resume data of any underlying network error. Only produced by `DownloadRequest`s.
public var downloadResumeData: Data? {
(underlyingError as? URLError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
}
#endif
}
extension AFError.ParameterEncodingFailureReason {
var underlyingError: Error? {
switch self {
case let .jsonEncodingFailed(error),
let .customEncodingFailed(error):
return error
case .missingURL:
return nil
}
}
}
extension AFError.ParameterEncoderFailureReason {
var underlyingError: Error? {
switch self {
case let .encoderFailed(error):
return error
case .missingRequiredComponent:
return nil
}
}
}
extension AFError.MultipartEncodingFailureReason {
var url: URL? {
switch self {
case let .bodyPartURLInvalid(url),
let .bodyPartFilenameInvalid(url),
let .bodyPartFileNotReachable(url),
let .bodyPartFileIsDirectory(url),
let .bodyPartFileSizeNotAvailable(url),
let .bodyPartInputStreamCreationFailed(url),
let .outputStreamCreationFailed(url),
let .outputStreamFileAlreadyExists(url),
let .outputStreamURLInvalid(url),
let .bodyPartFileNotReachableWithError(url, _),
let .bodyPartFileSizeQueryFailedWithError(url, _):
return url
case .outputStreamWriteFailed,
.inputStreamReadFailed:
return nil
}
}
var underlyingError: Error? {
switch self {
case let .bodyPartFileNotReachableWithError(_, error),
let .bodyPartFileSizeQueryFailedWithError(_, error),
let .outputStreamWriteFailed(error),
let .inputStreamReadFailed(error):
return error
case .bodyPartURLInvalid,
.bodyPartFilenameInvalid,
.bodyPartFileNotReachable,
.bodyPartFileIsDirectory,
.bodyPartFileSizeNotAvailable,
.bodyPartInputStreamCreationFailed,
.outputStreamCreationFailed,
.outputStreamFileAlreadyExists,
.outputStreamURLInvalid:
return nil
}
}
}
extension AFError.ResponseValidationFailureReason {
var acceptableContentTypes: [String]? {
switch self {
case let .missingContentType(types),
let .unacceptableContentType(types, _):
return types
case .dataFileNil,
.dataFileReadFailed,
.unacceptableStatusCode,
.customValidationFailed:
return nil
}
}
var responseContentType: String? {
switch self {
case let .unacceptableContentType(_, responseType):
return responseType
case .dataFileNil,
.dataFileReadFailed,
.missingContentType,
.unacceptableStatusCode,
.customValidationFailed:
return nil
}
}
var responseCode: Int? {
switch self {
case let .unacceptableStatusCode(code):
return code
case .dataFileNil,
.dataFileReadFailed,
.missingContentType,
.unacceptableContentType,
.customValidationFailed:
return nil
}
}
var underlyingError: Error? {
switch self {
case let .customValidationFailed(error):
return error
case .dataFileNil,
.dataFileReadFailed,
.missingContentType,
.unacceptableContentType,
.unacceptableStatusCode:
return nil
}
}
}
extension AFError.ResponseSerializationFailureReason {
var failedStringEncoding: String.Encoding? {
switch self {
case let .stringSerializationFailed(encoding):
return encoding
case .inputDataNilOrZeroLength,
.inputFileNil,
.inputFileReadFailed(_),
.jsonSerializationFailed(_),
.decodingFailed(_),
.customSerializationFailed(_),
.invalidEmptyResponse:
return nil
}
}
var underlyingError: Error? {
switch self {
case let .jsonSerializationFailed(error),
let .decodingFailed(error),
let .customSerializationFailed(error):
return error
case .inputDataNilOrZeroLength,
.inputFileNil,
.inputFileReadFailed,
.stringSerializationFailed,
.invalidEmptyResponse:
return nil
}
}
}
#if canImport(Security)
extension AFError.ServerTrustFailureReason {
var output: AFError.ServerTrustFailureReason.Output? {
switch self {
case let .defaultEvaluationFailed(output),
let .hostValidationFailed(output),
let .revocationCheckFailed(output, _):
return output
case .noRequiredEvaluator,
.noCertificatesFound,
.noPublicKeysFound,
.policyApplicationFailed,
.settingAnchorCertificatesFailed,
.revocationPolicyCreationFailed,
.trustEvaluationFailed,
.certificatePinningFailed,
.publicKeyPinningFailed,
.customEvaluationFailed:
return nil
}
}
var underlyingError: Error? {
switch self {
case let .customEvaluationFailed(error):
return error
case let .trustEvaluationFailed(error):
return error
case .noRequiredEvaluator,
.noCertificatesFound,
.noPublicKeysFound,
.policyApplicationFailed,
.settingAnchorCertificatesFailed,
.revocationPolicyCreationFailed,
.defaultEvaluationFailed,
.hostValidationFailed,
.revocationCheckFailed,
.certificatePinningFailed,
.publicKeyPinningFailed:
return nil
}
}
}
#endif
// MARK: - Error Descriptions
extension AFError: LocalizedError {
public var errorDescription: String? {
switch self {
case .explicitlyCancelled:
return "Request explicitly cancelled."
case let .invalidURL(url):
return "URL is not valid: \(url)"
case let .parameterEncodingFailed(reason):
return reason.localizedDescription
case let .parameterEncoderFailed(reason):
return reason.localizedDescription
case let .multipartEncodingFailed(reason):
return reason.localizedDescription
case let .requestAdaptationFailed(error):
return "Request adaption failed with error: \(error.localizedDescription)"
case let .responseValidationFailed(reason):
return reason.localizedDescription
case let .responseSerializationFailed(reason):
return reason.localizedDescription
case let .requestRetryFailed(retryError, originalError):
return """
Request retry failed with retry error: \(retryError.localizedDescription), \
original error: \(originalError.localizedDescription)
"""
case .sessionDeinitialized:
return """
Session was invalidated without error, so it was likely deinitialized unexpectedly. \
Be sure to retain a reference to your Session for the duration of your requests.
"""
case let .sessionInvalidated(error):
return "Session was invalidated with error: \(error?.localizedDescription ?? "No description.")"
#if canImport(Security)
case let .serverTrustEvaluationFailed(reason):
return "Server trust evaluation failed due to reason: \(reason.localizedDescription)"
#endif
case let .urlRequestValidationFailed(reason):
return "URLRequest validation failed due to reason: \(reason.localizedDescription)"
case let .createUploadableFailed(error):
return "Uploadable creation failed with error: \(error.localizedDescription)"
case let .createURLRequestFailed(error):
return "URLRequest creation failed with error: \(error.localizedDescription)"
case let .downloadedFileMoveFailed(error, source, destination):
return "Moving downloaded file from: \(source) to: \(destination) failed with error: \(error.localizedDescription)"
case let .sessionTaskFailed(error):
return "URLSessionTask failed with error: \(error.localizedDescription)"
}
}
}
extension AFError.ParameterEncodingFailureReason {
var localizedDescription: String {
switch self {
case .missingURL:
return "URL request to encode was missing a URL"
case let .jsonEncodingFailed(error):
return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
case let .customEncodingFailed(error):
return "Custom parameter encoder failed with error: \(error.localizedDescription)"
}
}
}
extension AFError.ParameterEncoderFailureReason {
var localizedDescription: String {
switch self {
case let .missingRequiredComponent(component):
return "Encoding failed due to a missing request component: \(component)"
case let .encoderFailed(error):
return "The underlying encoder failed with the error: \(error)"
}
}
}
extension AFError.MultipartEncodingFailureReason {
var localizedDescription: String {
switch self {
case let .bodyPartURLInvalid(url):
return "The URL provided is not a file URL: \(url)"
case let .bodyPartFilenameInvalid(url):
return "The URL provided does not have a valid filename: \(url)"
case let .bodyPartFileNotReachable(url):
return "The URL provided is not reachable: \(url)"
case let .bodyPartFileNotReachableWithError(url, error):
return """
The system returned an error while checking the provided URL for reachability.
URL: \(url)
Error: \(error)
"""
case let .bodyPartFileIsDirectory(url):
return "The URL provided is a directory: \(url)"
case let .bodyPartFileSizeNotAvailable(url):
return "Could not fetch the file size from the provided URL: \(url)"
case let .bodyPartFileSizeQueryFailedWithError(url, error):
return """
The system returned an error while attempting to fetch the file size from the provided URL.
URL: \(url)
Error: \(error)
"""
case let .bodyPartInputStreamCreationFailed(url):
return "Failed to create an InputStream for the provided URL: \(url)"
case let .outputStreamCreationFailed(url):
return "Failed to create an OutputStream for URL: \(url)"
case let .outputStreamFileAlreadyExists(url):
return "A file already exists at the provided URL: \(url)"
case let .outputStreamURLInvalid(url):
return "The provided OutputStream URL is invalid: \(url)"
case let .outputStreamWriteFailed(error):
return "OutputStream write failed with error: \(error)"
case let .inputStreamReadFailed(error):
return "InputStream read failed with error: \(error)"
}
}
}
extension AFError.ResponseSerializationFailureReason {
var localizedDescription: String {
switch self {
case .inputDataNilOrZeroLength:
return "Response could not be serialized, input data was nil or zero length."
case .inputFileNil:
return "Response could not be serialized, input file was nil."
case let .inputFileReadFailed(url):
return "Response could not be serialized, input file could not be read: \(url)."
case let .stringSerializationFailed(encoding):
return "String could not be serialized with encoding: \(encoding)."
case let .jsonSerializationFailed(error):
return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
case let .invalidEmptyResponse(type):
return """
Empty response could not be serialized to type: \(type). \
Use Empty as the expected type for such responses.
"""
case let .decodingFailed(error):
return "Response could not be decoded because of error:\n\(error.localizedDescription)"
case let .customSerializationFailed(error):
return "Custom response serializer failed with error:\n\(error.localizedDescription)"
}
}
}
extension AFError.ResponseValidationFailureReason {
var localizedDescription: String {
switch self {
case .dataFileNil:
return "Response could not be validated, data file was nil."
case let .dataFileReadFailed(url):
return "Response could not be validated, data file could not be read: \(url)."
case let .missingContentType(types):
return """
Response Content-Type was missing and acceptable content types \
(\(types.joined(separator: ","))) do not match "*/*".
"""
case let .unacceptableContentType(acceptableTypes, responseType):
return """
Response Content-Type "\(responseType)" does not match any acceptable types: \
\(acceptableTypes.joined(separator: ",")).
"""
case let .unacceptableStatusCode(code):
return "Response status code was unacceptable: \(code)."
case let .customValidationFailed(error):
return "Custom response validation failed with error: \(error.localizedDescription)"
}
}
}
#if canImport(Security)
extension AFError.ServerTrustFailureReason {
var localizedDescription: String {
switch self {
case let .noRequiredEvaluator(host):
return "A ServerTrustEvaluating value is required for host \(host) but none was found."
case .noCertificatesFound:
return "No certificates were found or provided for evaluation."
case .noPublicKeysFound:
return "No public keys were found or provided for evaluation."
case .policyApplicationFailed:
return "Attempting to set a SecPolicy failed."
case .settingAnchorCertificatesFailed:
return "Attempting to set the provided certificates as anchor certificates failed."
case .revocationPolicyCreationFailed:
return "Attempting to create a revocation policy failed."
case let .trustEvaluationFailed(error):
return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")"
case let .defaultEvaluationFailed(output):
return "Default evaluation failed for host \(output.host)."
case let .hostValidationFailed(output):
return "Host validation failed for host \(output.host)."
case let .revocationCheckFailed(output, _):
return "Revocation check failed for host \(output.host)."
case let .certificatePinningFailed(host, _, _, _):
return "Certificate pinning failed for host \(host)."
case let .publicKeyPinningFailed(host, _, _, _):
return "Public key pinning failed for host \(host)."
case let .customEvaluationFailed(error):
return "Custom trust evaluation failed with error: \(error.localizedDescription)"
}
}
}
#endif
extension AFError.URLRequestValidationFailureReason {
var localizedDescription: String {
switch self {
case let .bodyDataInGETRequest(data):
return """
Invalid URLRequest: Requests with GET method cannot have body data:
\(String(decoding: data, as: UTF8.self))
"""
}
}
}

View File

@ -0,0 +1,448 @@
//
// DataRequest.swift
//
// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`.
public class DataRequest: Request {
/// `URLRequestConvertible` value used to create `URLRequest`s for this instance.
public let convertible: URLRequestConvertible
/// `Data` read from the server so far.
public var data: Data? { dataMutableState.data }
private struct DataMutableState {
var data: Data?
var httpResponseHandler: (queue: DispatchQueue,
handler: (_ response: HTTPURLResponse,
_ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)?
}
private let dataMutableState = Protected(DataMutableState())
/// Creates a `DataRequest` using the provided parameters.
///
/// - Parameters:
/// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default.
/// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance.
/// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed.
/// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets
/// `underlyingQueue`, but can be passed another queue from a `Session`.
/// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions.
/// - interceptor: `RequestInterceptor` used throughout the request lifecycle.
/// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`.
init(id: UUID = UUID(),
convertible: URLRequestConvertible,
underlyingQueue: DispatchQueue,
serializationQueue: DispatchQueue,
eventMonitor: EventMonitor?,
interceptor: RequestInterceptor?,
delegate: RequestDelegate) {
self.convertible = convertible
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
override func reset() {
super.reset()
dataMutableState.write { mutableState in
mutableState.data = nil
}
}
/// Called when `Data` is received by this instance.
///
/// - Note: Also calls `updateDownloadProgress`.
///
/// - Parameter data: The `Data` received.
func didReceive(data: Data) {
dataMutableState.write { mutableState in
if mutableState.data == nil {
mutableState.data = data
} else {
mutableState.data?.append(data)
}
}
updateDownloadProgress()
}
func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
dataMutableState.read { dataMutableState in
guard let httpResponseHandler = dataMutableState.httpResponseHandler else {
underlyingQueue.async { completionHandler(.allow) }
return
}
httpResponseHandler.queue.async {
httpResponseHandler.handler(response) { disposition in
if disposition == .cancel {
self.mutableState.write { mutableState in
mutableState.state = .cancelled
mutableState.error = mutableState.error ?? AFError.explicitlyCancelled
}
}
self.underlyingQueue.async {
completionHandler(disposition.sessionDisposition)
}
}
}
}
}
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
/// Called to update the `downloadProgress` of the instance.
func updateDownloadProgress() {
let totalBytesReceived = Int64(data?.count ?? 0)
let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
downloadProgress.totalUnitCount = totalBytesExpected
downloadProgress.completedUnitCount = totalBytesReceived
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
/// Validates the request, using the specified closure.
///
/// - Note: If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter validation: `Validation` closure used to validate the response.
///
/// - Returns: The instance.
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void = { [unowned self] in
guard error == nil, let response else { return }
let result = validation(request, response, data)
if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) }
eventMonitor?.request(self,
didValidateRequest: request,
response: response,
data: data,
withResult: result)
}
validators.write { $0.append(validator) }
return self
}
/// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion
/// handler to return a `ResponseDisposition` value.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the closure will be called. `.main` by default.
/// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided
/// MUST be called, otherwise the request will never complete.
///
/// - Returns: The instance.
@_disfavoredOverload
@discardableResult
public func onHTTPResponse(
on queue: DispatchQueue = .main,
perform handler: @escaping (_ response: HTTPURLResponse,
_ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void
) -> Self {
dataMutableState.write { mutableState in
mutableState.httpResponseHandler = (queue, handler)
}
return self
}
/// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the closure will be called. `.main` by default.
/// - handler: Closure called when the instance produces an `HTTPURLResponse`.
///
/// - Returns: The instance.
@discardableResult
public func onHTTPResponse(on queue: DispatchQueue = .main,
perform handler: @escaping (HTTPURLResponse) -> Void) -> Self {
onHTTPResponse(on: queue) { response, completionHandler in
handler(response)
completionHandler(.allow)
}
return self
}
// MARK: Response Serialization
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self {
appendResponseSerializer {
// Start work that should be on the serialization queue.
let result = AFResult<Data?>(value: self.data, error: self.error)
// End work that should be on the serialization queue.
self.underlyingQueue.async {
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
private func _response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void)
-> Self {
appendResponseSerializer {
// Start work that should be on the serialization queue.
let start = ProcessInfo.processInfo.systemUptime
let result: AFResult<Serializer.SerializedObject> = Result {
try responseSerializer.serialize(request: self.request,
response: self.response,
data: self.data,
error: self.error)
}.mapError { error in
error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
}
let end = ProcessInfo.processInfo.systemUptime
// End work that should be on the serialization queue.
self.underlyingQueue.async {
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
guard !self.isCancelled, let serializerError = result.failure, let delegate = self.delegate else {
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
delegate.retryResult(for: self, dueTo: serializerError) { retryResult in
var didComplete: (() -> Void)?
defer {
if let didComplete {
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
switch retryResult {
case .doNotRetry:
didComplete = { completionHandler(response) }
case let .doNotRetryWithError(retryError):
let result: AFResult<Serializer.SerializedObject> = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError"))
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
didComplete = { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
}
}
return self
}
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default
/// - responseSerializer: The response serializer responsible for serializing the request, response, and data.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void)
-> Self {
_response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
}
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default
/// - responseSerializer: The response serializer responsible for serializing the request, response, and data.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response<Serializer: ResponseSerializer>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void)
-> Self {
_response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
}
/// Adds a handler using a `DataResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is called. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseData(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<Data>) -> Void) -> Self {
response(queue: queue,
responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
/// Adds a handler using a `StringResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined
/// from the server response, falling back to the default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseString(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<String>) -> Void) -> Self {
response(queue: queue,
responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
/// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments`
/// by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.")
@discardableResult
public func responseJSON(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods,
options: JSONSerialization.ReadingOptions = .allowFragments,
completionHandler: @escaping (AFDataResponse<Any>) -> Void) -> Self {
response(queue: queue,
responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods,
options: options),
completionHandler: completionHandler)
}
/// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - type: `Decodable` type to decode from response data.
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseDecodable<T: Decodable>(of type: T.Type = T.self,
queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self {
response(queue: queue,
responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}

View File

@ -0,0 +1,585 @@
//
// DataStreamRequest.swift
//
// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure.
public final class DataStreamRequest: Request {
/// Closure type handling `DataStreamRequest.Stream` values.
public typealias Handler<Success, Failure: Error> = (Stream<Success, Failure>) throws -> Void
/// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used
/// to stop the stream at any time.
public struct Stream<Success, Failure: Error> {
/// Latest `Event` from the stream.
public let event: Event<Success, Failure>
/// Token used to cancel the stream.
public let token: CancellationToken
/// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`.
public func cancel() {
token.cancel()
}
}
/// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed
/// `Data` or the completion of the stream.
public enum Event<Success, Failure: Error> {
/// Output produced every time the instance receives additional `Data`. The associated value contains the
/// `Result` of processing the incoming `Data`.
case stream(Result<Success, Failure>)
/// Output produced when the instance has completed, whether due to stream end, cancellation, or an error.
/// Associated `Completion` value contains the final state.
case complete(Completion)
}
/// Value containing the state of a `DataStreamRequest` when the stream was completed.
public struct Completion {
/// Last `URLRequest` issued by the instance.
public let request: URLRequest?
/// Last `HTTPURLResponse` received by the instance.
public let response: HTTPURLResponse?
/// Last `URLSessionTaskMetrics` produced for the instance.
public let metrics: URLSessionTaskMetrics?
/// `AFError` produced for the instance, if any.
public let error: AFError?
}
/// Type used to cancel an ongoing stream.
public struct CancellationToken {
weak var request: DataStreamRequest?
init(_ request: DataStreamRequest) {
self.request = request
}
/// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`.
public func cancel() {
request?.cancel()
}
}
/// `URLRequestConvertible` value used to create `URLRequest`s for this instance.
public let convertible: URLRequestConvertible
/// Whether or not the instance will be cancelled if stream parsing encounters an error.
public let automaticallyCancelOnStreamError: Bool
/// Internal mutable state specific to this type.
struct StreamMutableState {
/// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called.
var outputStream: OutputStream?
/// Stream closures called as `Data` is received.
var streams: [(_ data: Data) -> Void] = []
/// Number of currently executing streams. Used to ensure completions are only fired after all streams are
/// enqueued.
var numberOfExecutingStreams = 0
/// Completion calls enqueued while streams are still executing.
var enqueuedCompletionEvents: [() -> Void] = []
/// Handler for any `HTTPURLResponse`s received.
var httpResponseHandler: (queue: DispatchQueue,
handler: (_ response: HTTPURLResponse,
_ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)?
}
let streamMutableState = Protected(StreamMutableState())
/// Creates a `DataStreamRequest` using the provided parameters.
///
/// - Parameters:
/// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()`
/// by default.
/// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this
/// instance.
/// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error`
/// is thrown while serializing stream `Data`.
/// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed.
/// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default
/// targets
/// `underlyingQueue`, but can be passed another queue from a `Session`.
/// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions.
/// - interceptor: `RequestInterceptor` used throughout the request lifecycle.
/// - delegate: `RequestDelegate` that provides an interface to actions not performed by
/// the `Request`.
init(id: UUID = UUID(),
convertible: URLRequestConvertible,
automaticallyCancelOnStreamError: Bool,
underlyingQueue: DispatchQueue,
serializationQueue: DispatchQueue,
eventMonitor: EventMonitor?,
interceptor: RequestInterceptor?,
delegate: RequestDelegate) {
self.convertible = convertible
self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
override func finish(error: AFError? = nil) {
streamMutableState.write { state in
state.outputStream?.close()
}
super.finish(error: error)
}
func didReceive(data: Data) {
streamMutableState.write { state in
#if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation.
if let stream = state.outputStream {
underlyingQueue.async {
var bytes = Array(data)
stream.write(&bytes, maxLength: bytes.count)
}
}
#endif
state.numberOfExecutingStreams += state.streams.count
let localState = state
underlyingQueue.async { localState.streams.forEach { $0(data) } }
}
}
func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
streamMutableState.read { dataMutableState in
guard let httpResponseHandler = dataMutableState.httpResponseHandler else {
underlyingQueue.async { completionHandler(.allow) }
return
}
httpResponseHandler.queue.async {
httpResponseHandler.handler(response) { disposition in
if disposition == .cancel {
self.mutableState.write { mutableState in
mutableState.state = .cancelled
mutableState.error = mutableState.error ?? AFError.explicitlyCancelled
}
}
self.underlyingQueue.async {
completionHandler(disposition.sessionDisposition)
}
}
}
}
}
/// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure.
///
/// - Parameter validation: `Validation` closure used to validate the request and response.
///
/// - Returns: The `DataStreamRequest`.
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void = { [unowned self] in
guard error == nil, let response else { return }
let result = validation(request, response)
if case let .failure(error) = result {
self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
}
eventMonitor?.request(self,
didValidateRequest: request,
response: response,
withResult: result)
}
validators.write { $0.append(validator) }
return self
}
#if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation.
/// Produces an `InputStream` that receives the `Data` received by the instance.
///
/// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`.
/// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or
/// not the creating session has `startRequestsImmediately` set to `true`.
///
/// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`.
///
/// - Returns: The `InputStream` bound to the internal `OutboundStream`.
public func asInputStream(bufferSize: Int = 1024) -> InputStream? {
defer { resume() }
var inputStream: InputStream?
streamMutableState.write { state in
Foundation.Stream.getBoundStreams(withBufferSize: bufferSize,
inputStream: &inputStream,
outputStream: &state.outputStream)
state.outputStream?.open()
}
return inputStream
}
#endif
/// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion
/// handler to return a `ResponseDisposition` value.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the closure will be called. `.main` by default.
/// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided
/// MUST be called, otherwise the request will never complete.
///
/// - Returns: The instance.
@_disfavoredOverload
@discardableResult
public func onHTTPResponse(
on queue: DispatchQueue = .main,
perform handler: @escaping (_ response: HTTPURLResponse,
_ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void
) -> Self {
streamMutableState.write { mutableState in
mutableState.httpResponseHandler = (queue, handler)
}
return self
}
/// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which the closure will be called. `.main` by default.
/// - handler: Closure called when the instance produces an `HTTPURLResponse`.
///
/// - Returns: The instance.
@discardableResult
public func onHTTPResponse(on queue: DispatchQueue = .main,
perform handler: @escaping (HTTPURLResponse) -> Void) -> Self {
onHTTPResponse(on: queue) { response, completionHandler in
handler(response)
completionHandler(.allow)
}
return self
}
func capturingError(from closure: () throws -> Void) {
do {
try closure()
} catch {
self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
cancel()
}
}
func appendStreamCompletion<Success, Failure>(on queue: DispatchQueue,
stream: @escaping Handler<Success, Failure>) {
appendResponseSerializer {
self.underlyingQueue.async {
self.responseSerializerDidComplete {
self.streamMutableState.write { state in
guard state.numberOfExecutingStreams == 0 else {
state.enqueuedCompletionEvents.append {
self.enqueueCompletion(on: queue, stream: stream)
}
return
}
self.enqueueCompletion(on: queue, stream: stream)
}
}
}
}
}
func enqueueCompletion<Success, Failure>(on queue: DispatchQueue,
stream: @escaping Handler<Success, Failure>) {
queue.async {
do {
let completion = Completion(request: self.request,
response: self.response,
metrics: self.metrics,
error: self.error)
try stream(.init(event: .complete(completion), token: .init(self)))
} catch {
// Ignore error, as errors on Completion can't be handled anyway.
}
}
}
// MARK: Response Serialization
/// Adds a `StreamHandler` which performs no parsing on incoming `Data`.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which to perform `StreamHandler` closure.
/// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times.
///
/// - Returns: The `DataStreamRequest`.
@discardableResult
public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler<Data, Never>) -> Self {
let parser = { [unowned self] (data: Data) in
queue.async {
self.capturingError {
try stream(.init(event: .stream(.success(data)), token: .init(self)))
}
self.updateAndCompleteIfPossible()
}
}
streamMutableState.write { $0.streams.append(parser) }
appendStreamCompletion(on: queue, stream: stream)
return self
}
/// Adds a `StreamHandler` which uses the provided `DataStreamSerializer` to process incoming `Data`.
///
/// - Parameters:
/// - serializer: `DataStreamSerializer` used to process incoming `Data`. Its work is done on the `serializationQueue`.
/// - queue: `DispatchQueue` on which to perform `StreamHandler` closure.
/// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times.
///
/// - Returns: The `DataStreamRequest`.
@discardableResult
public func responseStream<Serializer: DataStreamSerializer>(using serializer: Serializer,
on queue: DispatchQueue = .main,
stream: @escaping Handler<Serializer.SerializedObject, AFError>) -> Self {
let parser = { [unowned self] (data: Data) in
serializationQueue.async {
// Start work on serialization queue.
let result = Result { try serializer.serialize(data) }
.mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) }
// End work on serialization queue.
self.underlyingQueue.async {
self.eventMonitor?.request(self, didParseStream: result)
if result.isFailure, self.automaticallyCancelOnStreamError {
self.cancel()
}
queue.async {
self.capturingError {
try stream(.init(event: .stream(result), token: .init(self)))
}
self.updateAndCompleteIfPossible()
}
}
}
}
streamMutableState.write { $0.streams.append(parser) }
appendStreamCompletion(on: queue, stream: stream)
return self
}
/// Adds a `StreamHandler` which parses incoming `Data` as a UTF8 `String`.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which to perform `StreamHandler` closure.
/// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times.
///
/// - Returns: The `DataStreamRequest`.
@discardableResult
public func responseStreamString(on queue: DispatchQueue = .main,
stream: @escaping Handler<String, Never>) -> Self {
let parser = { [unowned self] (data: Data) in
serializationQueue.async {
// Start work on serialization queue.
let string = String(decoding: data, as: UTF8.self)
// End work on serialization queue.
self.underlyingQueue.async {
self.eventMonitor?.request(self, didParseStream: .success(string))
queue.async {
self.capturingError {
try stream(.init(event: .stream(.success(string)), token: .init(self)))
}
self.updateAndCompleteIfPossible()
}
}
}
}
streamMutableState.write { $0.streams.append(parser) }
appendStreamCompletion(on: queue, stream: stream)
return self
}
private func updateAndCompleteIfPossible() {
streamMutableState.write { state in
state.numberOfExecutingStreams -= 1
guard state.numberOfExecutingStreams == 0, !state.enqueuedCompletionEvents.isEmpty else { return }
let completionEvents = state.enqueuedCompletionEvents
self.underlyingQueue.async { completionEvents.forEach { $0() } }
state.enqueuedCompletionEvents.removeAll()
}
}
/// Adds a `StreamHandler` which parses incoming `Data` using the provided `DataDecoder`.
///
/// - Parameters:
/// - type: `Decodable` type to parse incoming `Data` into.
/// - queue: `DispatchQueue` on which to perform `StreamHandler` closure.
/// - decoder: `DataDecoder` used to decode the incoming `Data`.
/// - preprocessor: `DataPreprocessor` used to process the incoming `Data` before it's passed to the `decoder`.
/// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times.
///
/// - Returns: The `DataStreamRequest`.
@discardableResult
public func responseStreamDecodable<T: Decodable>(of type: T.Type = T.self,
on queue: DispatchQueue = .main,
using decoder: DataDecoder = JSONDecoder(),
preprocessor: DataPreprocessor = PassthroughPreprocessor(),
stream: @escaping Handler<T, AFError>) -> Self {
responseStream(using: DecodableStreamSerializer<T>(decoder: decoder, dataPreprocessor: preprocessor),
on: queue,
stream: stream)
}
}
extension DataStreamRequest.Stream {
/// Incoming `Result` values from `Event.stream`.
public var result: Result<Success, Failure>? {
guard case let .stream(result) = event else { return nil }
return result
}
/// `Success` value of the instance, if any.
public var value: Success? {
guard case let .success(value) = result else { return nil }
return value
}
/// `Failure` value of the instance, if any.
public var error: Failure? {
guard case let .failure(error) = result else { return nil }
return error
}
/// `Completion` value of the instance, if any.
public var completion: DataStreamRequest.Completion? {
guard case let .complete(completion) = event else { return nil }
return completion
}
}
// MARK: - Serialization
/// A type which can serialize incoming `Data`.
public protocol DataStreamSerializer {
/// Type produced from the serialized `Data`.
associatedtype SerializedObject
/// Serializes incoming `Data` into a `SerializedObject` value.
///
/// - Parameter data: `Data` to be serialized.
///
/// - Throws: Any error produced during serialization.
func serialize(_ data: Data) throws -> SerializedObject
}
/// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`.
public struct DecodableStreamSerializer<T: Decodable>: DataStreamSerializer {
/// `DataDecoder` used to decode incoming `Data`.
public let decoder: DataDecoder
/// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`.
public let dataPreprocessor: DataPreprocessor
/// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`.
/// - Parameters:
/// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default.
/// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the
/// `decoder`. `PassthroughPreprocessor()` by default.
public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) {
self.decoder = decoder
self.dataPreprocessor = dataPreprocessor
}
public func serialize(_ data: Data) throws -> T {
let processedData = try dataPreprocessor.preprocess(data)
do {
return try decoder.decode(T.self, from: processedData)
} catch {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
}
}
}
/// `DataStreamSerializer` which performs no serialization on incoming `Data`.
public struct PassthroughStreamSerializer: DataStreamSerializer {
/// Creates an instance.
public init() {}
public func serialize(_ data: Data) throws -> Data { data }
}
/// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values.
public struct StringStreamSerializer: DataStreamSerializer {
/// Creates an instance.
public init() {}
public func serialize(_ data: Data) throws -> String {
String(decoding: data, as: UTF8.self)
}
}
extension DataStreamSerializer {
/// Creates a `DecodableStreamSerializer` instance with the provided `DataDecoder` and `DataPreprocessor`.
///
/// - Parameters:
/// - type: `Decodable` type to decode from stream data.
/// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default.
/// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the
/// `decoder`. `PassthroughPreprocessor()` by default.
public static func decodable<T: Decodable>(of type: T.Type,
decoder: DataDecoder = JSONDecoder(),
dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer<T> {
DecodableStreamSerializer<T>(decoder: decoder, dataPreprocessor: dataPreprocessor)
}
}
extension DataStreamSerializer where Self == PassthroughStreamSerializer {
/// Provides a `PassthroughStreamSerializer` instance.
public static var passthrough: PassthroughStreamSerializer { PassthroughStreamSerializer() }
}
extension DataStreamSerializer where Self == StringStreamSerializer {
/// Provides a `StringStreamSerializer` instance.
public static var string: StringStreamSerializer { StringStreamSerializer() }
}

View File

@ -0,0 +1,588 @@
//
// DownloadRequest.swift
//
// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`.
public final class DownloadRequest: Request {
/// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination
/// `URL`.
public struct Options: OptionSet {
/// Specifies that intermediate directories for the destination URL should be created.
public static let createIntermediateDirectories = Options(rawValue: 1 << 0)
/// Specifies that any previous file at the destination `URL` should be removed.
public static let removePreviousFile = Options(rawValue: 1 << 1)
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
// MARK: Destination
/// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the
/// temporary file written to during the download process. The closure takes two arguments: the temporary file URL
/// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and
/// the options defining how the file should be moved.
///
/// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not
/// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory.
public typealias Destination = (_ temporaryURL: URL,
_ response: HTTPURLResponse) -> (destinationURL: URL, options: Options)
/// Creates a download file destination closure which uses the default file manager to move the temporary file to a
/// file URL in the first available directory with the specified search path directory and search path domain mask.
///
/// - Parameters:
/// - directory: The search path directory. `.documentDirectory` by default.
/// - domain: The search path domain mask. `.userDomainMask` by default.
/// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by
/// default.
/// - Returns: The `Destination` closure.
public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory,
in domain: FileManager.SearchPathDomainMask = .userDomainMask,
options: Options = []) -> Destination {
{ temporaryURL, response in
let directoryURLs = FileManager.default.urls(for: directory, in: domain)
let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL
return (url, options)
}
}
/// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends
/// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files
/// with this destination must be additionally moved if they should survive the system reclamation of temporary
/// space.
static let defaultDestination: Destination = { url, _ in
(defaultDestinationURL(url), [])
}
/// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the
/// provided file name.
static let defaultDestinationURL: (URL) -> URL = { url in
let filename = "Alamofire_\(url.lastPathComponent)"
let destination = url.deletingLastPathComponent().appendingPathComponent(filename)
return destination
}
// MARK: Downloadable
/// Type describing the source used to create the underlying `URLSessionDownloadTask`.
public enum Downloadable {
/// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value.
case request(URLRequestConvertible)
/// Download should be started from the associated resume `Data` value.
case resumeData(Data)
}
// MARK: Mutable State
/// Type containing all mutable state for `DownloadRequest` instances.
private struct DownloadRequestMutableState {
/// Possible resume `Data` produced when cancelling the instance.
var resumeData: Data?
/// `URL` to which `Data` is being downloaded.
var fileURL: URL?
}
/// Protected mutable state specific to `DownloadRequest`.
private let mutableDownloadState = Protected(DownloadRequestMutableState())
/// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download
/// using the `download(resumingWith data:)` API.
///
/// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel).
public var resumeData: Data? {
#if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation.
return mutableDownloadState.resumeData ?? error?.downloadResumeData
#else
return mutableDownloadState.resumeData
#endif
}
/// If the download is successful, the `URL` where the file was downloaded.
public var fileURL: URL? { mutableDownloadState.fileURL }
// MARK: Initial State
/// `Downloadable` value used for this instance.
public let downloadable: Downloadable
/// The `Destination` to which the downloaded file is moved.
let destination: Destination
/// Creates a `DownloadRequest` using the provided parameters.
///
/// - Parameters:
/// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default.
/// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance.
/// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed.
/// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets
/// `underlyingQueue`, but can be passed another queue from a `Session`.
/// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions.
/// - interceptor: `RequestInterceptor` used throughout the request lifecycle.
/// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`
/// - destination: `Destination` closure used to move the downloaded file to its final location.
init(id: UUID = UUID(),
downloadable: Downloadable,
underlyingQueue: DispatchQueue,
serializationQueue: DispatchQueue,
eventMonitor: EventMonitor?,
interceptor: RequestInterceptor?,
delegate: RequestDelegate,
destination: @escaping Destination) {
self.downloadable = downloadable
self.destination = destination
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
override func reset() {
super.reset()
mutableDownloadState.write {
$0.resumeData = nil
$0.fileURL = nil
}
}
/// Called when a download has finished.
///
/// - Parameters:
/// - task: `URLSessionTask` that finished the download.
/// - result: `Result` of the automatic move to `destination`.
func didFinishDownloading(using task: URLSessionTask, with result: Result<URL, AFError>) {
eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result)
switch result {
case let .success(url): mutableDownloadState.fileURL = url
case let .failure(error): self.error = error
}
}
/// Updates the `downloadProgress` using the provided values.
///
/// - Parameters:
/// - bytesWritten: Total bytes written so far.
/// - totalBytesExpectedToWrite: Total bytes expected to write.
func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadProgress.totalUnitCount = totalBytesExpectedToWrite
downloadProgress.completedUnitCount += bytesWritten
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
session.downloadTask(with: request)
}
/// Creates a `URLSessionTask` from the provided resume data.
///
/// - Parameters:
/// - data: `Data` used to resume the download.
/// - session: `URLSession` used to create the `URLSessionTask`.
///
/// - Returns: The `URLSessionTask` created.
public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask {
session.downloadTask(withResumeData: data)
}
/// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended.
///
/// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use
/// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`.
///
/// - Returns: The instance.
@discardableResult
override public func cancel() -> Self {
cancel(producingResumeData: false)
}
/// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be
/// resumed or suspended.
///
/// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if
/// available.
///
/// - Returns: The instance.
@discardableResult
public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self {
cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil)
}
/// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed
/// or suspended.
///
/// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData`
/// property.
///
/// - Parameter completionHandler: The completion handler that is called when the download has been successfully
/// cancelled. It is not guaranteed to be called on a particular queue, so you may
/// want use an appropriate queue to perform your work.
///
/// - Returns: The instance.
@discardableResult
public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self {
cancel(optionallyProducingResumeData: completionHandler)
}
/// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed,
/// cancellation is performed without producing resume data.
///
/// - Parameter completionHandler: Optional resume data handler.
///
/// - Returns: The instance.
private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self {
mutableState.write { mutableState in
guard mutableState.state.canTransitionTo(.cancelled) else { return }
mutableState.state = .cancelled
underlyingQueue.async { self.didCancel() }
guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else {
underlyingQueue.async { self.finish() }
return
}
if let completionHandler {
// Resume to ensure metrics are gathered.
task.resume()
task.cancel { resumeData in
self.mutableDownloadState.resumeData = resumeData
self.underlyingQueue.async { self.didCancelTask(task) }
completionHandler(resumeData)
}
} else {
// Resume to ensure metrics are gathered.
task.resume()
task.cancel()
self.underlyingQueue.async { self.didCancelTask(task) }
}
}
return self
}
/// Validates the request, using the specified closure.
///
/// - Note: If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter validation: `Validation` closure to validate the response.
///
/// - Returns: The instance.
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void = { [unowned self] in
guard error == nil, let response else { return }
let result = validation(request, response, fileURL)
if case let .failure(error) = result {
self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
}
eventMonitor?.request(self,
didValidateRequest: request,
response: response,
fileURL: fileURL,
withResult: result)
}
validators.write { $0.append(validator) }
return self
}
// MARK: - Response Serialization
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response(queue: DispatchQueue = .main,
completionHandler: @escaping (AFDownloadResponse<URL?>) -> Void)
-> Self {
appendResponseSerializer {
// Start work that should be on the serialization queue.
let result = AFResult<URL?>(value: self.fileURL, error: self.error)
// End work that should be on the serialization queue.
self.underlyingQueue.async {
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
private func _response<Serializer: DownloadResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void)
-> Self {
appendResponseSerializer {
// Start work that should be on the serialization queue.
let start = ProcessInfo.processInfo.systemUptime
let result: AFResult<Serializer.SerializedObject> = Result {
try responseSerializer.serializeDownload(request: self.request,
response: self.response,
fileURL: self.fileURL,
error: self.error)
}.mapError { error in
error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
}
let end = ProcessInfo.processInfo.systemUptime
// End work that should be on the serialization queue.
self.underlyingQueue.async {
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
guard let serializerError = result.failure, let delegate = self.delegate else {
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
delegate.retryResult(for: self, dueTo: serializerError) { retryResult in
var didComplete: (() -> Void)?
defer {
if let didComplete {
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
switch retryResult {
case .doNotRetry:
didComplete = { completionHandler(response) }
case let .doNotRetryWithError(retryError):
let result: AFResult<Serializer.SerializedObject> = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError"))
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
didComplete = { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
}
}
return self
}
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - responseSerializer: The response serializer responsible for serializing the request, response, and data
/// contained in the destination `URL`.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response<Serializer: DownloadResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void)
-> Self {
_response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
}
/// Adds a handler to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - responseSerializer: The response serializer responsible for serializing the request, response, and data
/// contained in the destination `URL`.
/// - completionHandler: The code to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func response<Serializer: ResponseSerializer>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void)
-> Self {
_response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
}
/// Adds a handler using a `URLResponseSerializer` to be called once the request is finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is called. `.main` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseURL(queue: DispatchQueue = .main,
completionHandler: @escaping (AFDownloadResponse<URL>) -> Void) -> Self {
response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler)
}
/// Adds a handler using a `DataResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is called. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseData(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDownloadResponse<Data>) -> Void) -> Self {
response(queue: queue,
responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
/// Adds a handler using a `StringResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined
/// from the server response, falling back to the default HTTP character set, `ISO-8859-1`.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseString(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil,
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDownloadResponse<String>) -> Void) -> Self {
response(queue: queue,
responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
/// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments`
/// by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.")
@discardableResult
public func responseJSON(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor,
emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods,
options: JSONSerialization.ReadingOptions = .allowFragments,
completionHandler: @escaping (AFDownloadResponse<Any>) -> Void) -> Self {
response(queue: queue,
responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods,
options: options),
completionHandler: completionHandler)
}
/// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished.
///
/// - Parameters:
/// - type: `Decodable` type to decode from response data.
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
/// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the
/// `completionHandler`. `PassthroughPreprocessor()` by default.
/// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
/// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default.
/// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
/// - completionHandler: A closure to be executed once the request has finished.
///
/// - Returns: The request.
@discardableResult
public func responseDecodable<T: Decodable>(of type: T.Type = T.self,
queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDownloadResponse<T>) -> Void) -> Self {
response(queue: queue,
responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}

View File

@ -0,0 +1,466 @@
//
// HTTPHeaders.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// An order-preserving and case-insensitive representation of HTTP headers.
public struct HTTPHeaders: Equatable, Hashable, Sendable {
private var headers: [HTTPHeader] = []
/// Creates an empty instance.
public init() {}
/// Creates an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last
/// name and value encountered.
public init(_ headers: [HTTPHeader]) {
headers.forEach { update($0) }
}
/// Creates an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name
/// and value encountered.
public init(_ dictionary: [String: String]) {
dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) }
}
/// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`.
///
/// - Parameters:
/// - name: The `HTTPHeader` name.
/// - value: The `HTTPHeader` value.
public mutating func add(name: String, value: String) {
update(HTTPHeader(name: name, value: value))
}
/// Case-insensitively updates or appends the provided `HTTPHeader` into the instance.
///
/// - Parameter header: The `HTTPHeader` to update or append.
public mutating func add(_ header: HTTPHeader) {
update(header)
}
/// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`.
///
/// - Parameters:
/// - name: The `HTTPHeader` name.
/// - value: The `HTTPHeader` value.
public mutating func update(name: String, value: String) {
update(HTTPHeader(name: name, value: value))
}
/// Case-insensitively updates or appends the provided `HTTPHeader` into the instance.
///
/// - Parameter header: The `HTTPHeader` to update or append.
public mutating func update(_ header: HTTPHeader) {
guard let index = headers.index(of: header.name) else {
headers.append(header)
return
}
headers.replaceSubrange(index...index, with: [header])
}
/// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance.
///
/// - Parameter name: The name of the `HTTPHeader` to remove.
public mutating func remove(name: String) {
guard let index = headers.index(of: name) else { return }
headers.remove(at: index)
}
/// Sort the current instance by header name, case insensitively.
public mutating func sort() {
headers.sort { $0.name.lowercased() < $1.name.lowercased() }
}
/// Returns an instance sorted by header name.
///
/// - Returns: A copy of the current instance sorted by name.
public func sorted() -> HTTPHeaders {
var headers = self
headers.sort()
return headers
}
/// Case-insensitively find a header's value by name.
///
/// - Parameter name: The name of the header to search for, case-insensitively.
///
/// - Returns: The value of header, if it exists.
public func value(for name: String) -> String? {
guard let index = headers.index(of: name) else { return nil }
return headers[index].value
}
/// Case-insensitively access the header with the given name.
///
/// - Parameter name: The name of the header.
public subscript(_ name: String) -> String? {
get { value(for: name) }
set {
if let value = newValue {
update(name: name, value: value)
} else {
remove(name: name)
}
}
}
/// The dictionary representation of all headers.
///
/// This representation does not preserve the current order of the instance.
public var dictionary: [String: String] {
let namesAndValues = headers.map { ($0.name, $0.value) }
return Dictionary(namesAndValues, uniquingKeysWith: { _, last in last })
}
}
extension HTTPHeaders: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, String)...) {
elements.forEach { update(name: $0.0, value: $0.1) }
}
}
extension HTTPHeaders: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: HTTPHeader...) {
self.init(elements)
}
}
extension HTTPHeaders: Sequence {
public func makeIterator() -> IndexingIterator<[HTTPHeader]> {
headers.makeIterator()
}
}
extension HTTPHeaders: Collection {
public var startIndex: Int {
headers.startIndex
}
public var endIndex: Int {
headers.endIndex
}
public subscript(position: Int) -> HTTPHeader {
headers[position]
}
public func index(after i: Int) -> Int {
headers.index(after: i)
}
}
extension HTTPHeaders: CustomStringConvertible {
public var description: String {
headers.map(\.description)
.joined(separator: "\n")
}
}
// MARK: - HTTPHeader
/// A representation of a single HTTP header's name / value pair.
public struct HTTPHeader: Equatable, Hashable, Sendable {
/// Name of the header.
public let name: String
/// Value of the header.
public let value: String
/// Creates an instance from the given `name` and `value`.
///
/// - Parameters:
/// - name: The name of the header.
/// - value: The value of the header.
public init(name: String, value: String) {
self.name = name
self.value = value
}
}
extension HTTPHeader: CustomStringConvertible {
public var description: String {
"\(name): \(value)"
}
}
extension HTTPHeader {
/// Returns an `Accept` header.
///
/// - Parameter value: The `Accept` value.
/// - Returns: The header.
public static func accept(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Accept", value: value)
}
/// Returns an `Accept-Charset` header.
///
/// - Parameter value: The `Accept-Charset` value.
/// - Returns: The header.
public static func acceptCharset(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Accept-Charset", value: value)
}
/// Returns an `Accept-Language` header.
///
/// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages.
/// Use `HTTPHeader.defaultAcceptLanguage`.
///
/// - Parameter value: The `Accept-Language` value.
///
/// - Returns: The header.
public static func acceptLanguage(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Accept-Language", value: value)
}
/// Returns an `Accept-Encoding` header.
///
/// Alamofire offers a default accept encoding value that provides the most common values. Use
/// `HTTPHeader.defaultAcceptEncoding`.
///
/// - Parameter value: The `Accept-Encoding` value.
///
/// - Returns: The header
public static func acceptEncoding(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Accept-Encoding", value: value)
}
/// Returns a `Basic` `Authorization` header using the `username` and `password` provided.
///
/// - Parameters:
/// - username: The username of the header.
/// - password: The password of the header.
///
/// - Returns: The header.
public static func authorization(username: String, password: String) -> HTTPHeader {
let credential = Data("\(username):\(password)".utf8).base64EncodedString()
return authorization("Basic \(credential)")
}
/// Returns a `Bearer` `Authorization` header using the `bearerToken` provided.
///
/// - Parameter bearerToken: The bearer token.
///
/// - Returns: The header.
public static func authorization(bearerToken: String) -> HTTPHeader {
authorization("Bearer \(bearerToken)")
}
/// Returns an `Authorization` header.
///
/// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use
/// `HTTPHeader.authorization(username:password:)`. For a Bearer `Authorization` header, use
/// `HTTPHeader.authorization(bearerToken:)`.
///
/// - Parameter value: The `Authorization` value.
///
/// - Returns: The header.
public static func authorization(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Authorization", value: value)
}
/// Returns a `Content-Disposition` header.
///
/// - Parameter value: The `Content-Disposition` value.
///
/// - Returns: The header.
public static func contentDisposition(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Content-Disposition", value: value)
}
/// Returns a `Content-Encoding` header.
///
/// - Parameter value: The `Content-Encoding`.
///
/// - Returns: The header.
public static func contentEncoding(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Content-Encoding", value: value)
}
/// Returns a `Content-Type` header.
///
/// All Alamofire `ParameterEncoding`s and `ParameterEncoder`s set the `Content-Type` of the request, so it may not
/// be necessary to manually set this value.
///
/// - Parameter value: The `Content-Type` value.
///
/// - Returns: The header.
public static func contentType(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Content-Type", value: value)
}
/// Returns a `User-Agent` header.
///
/// - Parameter value: The `User-Agent` value.
///
/// - Returns: The header.
public static func userAgent(_ value: String) -> HTTPHeader {
HTTPHeader(name: "User-Agent", value: value)
}
/// Returns a `Sec-WebSocket-Protocol` header.
///
/// - Parameter value: The `Sec-WebSocket-Protocol` value.
/// - Returns: The header.
public static func websocketProtocol(_ value: String) -> HTTPHeader {
HTTPHeader(name: "Sec-WebSocket-Protocol", value: value)
}
}
extension [HTTPHeader] {
/// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists.
func index(of name: String) -> Int? {
let lowercasedName = name.lowercased()
return firstIndex { $0.name.lowercased() == lowercasedName }
}
}
// MARK: - Defaults
extension HTTPHeaders {
/// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and
/// `User-Agent`.
public static let `default`: HTTPHeaders = [.defaultAcceptEncoding,
.defaultAcceptLanguage,
.defaultUserAgent]
}
extension HTTPHeader {
/// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supported by particular OS
/// versions.
///
/// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) .
public static let defaultAcceptEncoding: HTTPHeader = {
let encodings: [String]
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
encodings = ["br", "gzip", "deflate"]
} else {
encodings = ["gzip", "deflate"]
}
return .acceptEncoding(encodings.qualityEncoded())
}()
/// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's
/// `preferredLanguages`.
///
/// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5).
public static let defaultAcceptLanguage: HTTPHeader = .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded())
/// Returns Alamofire's default `User-Agent` header.
///
/// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3).
///
/// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0`
public static let defaultUserAgent: HTTPHeader = {
let info = Bundle.main.infoDictionary
let executable = (info?["CFBundleExecutable"] as? String) ??
(ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ??
"Unknown"
let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown"
let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown"
let osNameVersion: String = {
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
let osName: String = {
#if os(iOS)
#if targetEnvironment(macCatalyst)
return "macOS(Catalyst)"
#else
return "iOS"
#endif
#elseif os(watchOS)
return "watchOS"
#elseif os(tvOS)
return "tvOS"
#elseif os(macOS)
#if targetEnvironment(macCatalyst)
return "macOS(Catalyst)"
#else
return "macOS"
#endif
#elseif swift(>=5.9.2) && os(visionOS)
return "visionOS"
#elseif os(Linux)
return "Linux"
#elseif os(Windows)
return "Windows"
#elseif os(Android)
return "Android"
#else
return "Unknown"
#endif
}()
return "\(osName) \(versionString)"
}()
let alamofireVersion = "Alamofire/\(AFInfo.version)"
let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
return .userAgent(userAgent)
}()
}
extension Collection<String> {
func qualityEncoded() -> String {
enumerated().map { index, encoding in
let quality = 1.0 - (Double(index) * 0.1)
return "\(encoding);q=\(quality)"
}.joined(separator: ", ")
}
}
// MARK: - System Type Extensions
extension URLRequest {
/// Returns `allHTTPHeaderFields` as `HTTPHeaders`.
public var headers: HTTPHeaders {
get { allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() }
set { allHTTPHeaderFields = newValue.dictionary }
}
}
extension HTTPURLResponse {
/// Returns `allHeaderFields` as `HTTPHeaders`.
public var headers: HTTPHeaders {
(allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders()
}
}
extension URLSessionConfiguration {
/// Returns `httpAdditionalHeaders` as `HTTPHeaders`.
public var headers: HTTPHeaders {
get { (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() }
set { httpAdditionalHeaders = newValue.dictionary }
}
}

View File

@ -0,0 +1,56 @@
//
// HTTPMethod.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Type representing HTTP methods. Raw `String` value is stored and compared case-sensitively, so
/// `HTTPMethod.get != HTTPMethod(rawValue: "get")`.
///
/// See https://tools.ietf.org/html/rfc7231#section-4.3
public struct HTTPMethod: RawRepresentable, Equatable, Hashable, Sendable {
/// `CONNECT` method.
public static let connect = HTTPMethod(rawValue: "CONNECT")
/// `DELETE` method.
public static let delete = HTTPMethod(rawValue: "DELETE")
/// `GET` method.
public static let get = HTTPMethod(rawValue: "GET")
/// `HEAD` method.
public static let head = HTTPMethod(rawValue: "HEAD")
/// `OPTIONS` method.
public static let options = HTTPMethod(rawValue: "OPTIONS")
/// `PATCH` method.
public static let patch = HTTPMethod(rawValue: "PATCH")
/// `POST` method.
public static let post = HTTPMethod(rawValue: "POST")
/// `PUT` method.
public static let put = HTTPMethod(rawValue: "PUT")
/// `QUERY` method.
public static let query = HTTPMethod(rawValue: "QUERY")
/// `TRACE` method.
public static let trace = HTTPMethod(rawValue: "TRACE")
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}

View File

@ -0,0 +1,115 @@
//
// Notifications.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
extension Request {
/// Posted when a `Request` is resumed. The `Notification` contains the resumed `Request`.
public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume")
/// Posted when a `Request` is suspended. The `Notification` contains the suspended `Request`.
public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend")
/// Posted when a `Request` is cancelled. The `Notification` contains the cancelled `Request`.
public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel")
/// Posted when a `Request` is finished. The `Notification` contains the completed `Request`.
public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish")
/// Posted when a `URLSessionTask` is resumed. The `Notification` contains the `Request` associated with the `URLSessionTask`.
public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask")
/// Posted when a `URLSessionTask` is suspended. The `Notification` contains the `Request` associated with the `URLSessionTask`.
public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask")
/// Posted when a `URLSessionTask` is cancelled. The `Notification` contains the `Request` associated with the `URLSessionTask`.
public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask")
/// Posted when a `URLSessionTask` is completed. The `Notification` contains the `Request` associated with the `URLSessionTask`.
public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask")
}
// MARK: -
extension Notification {
/// The `Request` contained by the instance's `userInfo`, `nil` otherwise.
public var request: Request? {
userInfo?[String.requestKey] as? Request
}
/// Convenience initializer for a `Notification` containing a `Request` payload.
///
/// - Parameters:
/// - name: The name of the notification.
/// - request: The `Request` payload.
init(name: Notification.Name, request: Request) {
self.init(name: name, object: nil, userInfo: [String.requestKey: request])
}
}
extension NotificationCenter {
/// Convenience function for posting notifications with `Request` payloads.
///
/// - Parameters:
/// - name: The name of the notification.
/// - request: The `Request` payload.
func postNotification(named name: Notification.Name, with request: Request) {
let notification = Notification(name: name, request: request)
post(notification)
}
}
extension String {
/// User info dictionary key representing the `Request` associated with the notification.
fileprivate static let requestKey = "org.alamofire.notification.key.request"
}
/// `EventMonitor` that provides Alamofire's notifications.
public final class AlamofireNotifications: EventMonitor {
public func requestDidResume(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request)
}
public func requestDidSuspend(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request)
}
public func requestDidCancel(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request)
}
public func requestDidFinish(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request)
}
public func request(_ request: Request, didResumeTask task: URLSessionTask) {
NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request)
}
public func request(_ request: Request, didSuspendTask task: URLSessionTask) {
NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request)
}
public func request(_ request: Request, didCancelTask task: URLSessionTask) {
NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request)
}
public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {
NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request)
}
}

View File

@ -0,0 +1,213 @@
//
// ParameterEncoder.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A type that can encode any `Encodable` type into a `URLRequest`.
public protocol ParameterEncoder {
/// Encode the provided `Encodable` parameters into `request`.
///
/// - Parameters:
/// - parameters: The `Encodable` parameter value.
/// - request: The `URLRequest` into which to encode the parameters.
///
/// - Returns: A `URLRequest` with the result of the encoding.
/// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of
/// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`.
func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest
}
/// A `ParameterEncoder` that encodes types as JSON body data.
///
/// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`.
open class JSONParameterEncoder: ParameterEncoder {
/// Returns an encoder with default parameters.
public static var `default`: JSONParameterEncoder { JSONParameterEncoder() }
/// Returns an encoder with `JSONEncoder.outputFormatting` set to `.prettyPrinted`.
public static var prettyPrinted: JSONParameterEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return JSONParameterEncoder(encoder: encoder)
}
/// Returns an encoder with `JSONEncoder.outputFormatting` set to `.sortedKeys`.
@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
public static var sortedKeys: JSONParameterEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
return JSONParameterEncoder(encoder: encoder)
}
/// `JSONEncoder` used to encode parameters.
public let encoder: JSONEncoder
/// Creates an instance with the provided `JSONEncoder`.
///
/// - Parameter encoder: The `JSONEncoder`. `JSONEncoder()` by default.
public init(encoder: JSONEncoder = JSONEncoder()) {
self.encoder = encoder
}
open func encode<Parameters: Encodable>(_ parameters: Parameters?,
into request: URLRequest) throws -> URLRequest {
guard let parameters else { return request }
var request = request
do {
let data = try encoder.encode(parameters)
request.httpBody = data
if request.headers["Content-Type"] == nil {
request.headers.update(.contentType("application/json"))
}
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return request
}
}
extension ParameterEncoder where Self == JSONParameterEncoder {
/// Provides a default `JSONParameterEncoder` instance.
public static var json: JSONParameterEncoder { JSONParameterEncoder() }
/// Creates a `JSONParameterEncoder` using the provided `JSONEncoder`.
///
/// - Parameter encoder: `JSONEncoder` used to encode parameters. `JSONEncoder()` by default.
/// - Returns: The `JSONParameterEncoder`.
public static func json(encoder: JSONEncoder = JSONEncoder()) -> JSONParameterEncoder {
JSONParameterEncoder(encoder: encoder)
}
}
/// A `ParameterEncoder` that encodes types as URL-encoded query strings to be set on the URL or as body data, depending
/// on the `Destination` set.
///
/// If no `Content-Type` header is already set on the provided `URLRequest`s, it will be set to
/// `application/x-www-form-urlencoded; charset=utf-8`.
///
/// Encoding behavior can be customized by passing an instance of `URLEncodedFormEncoder` to the initializer.
open class URLEncodedFormParameterEncoder: ParameterEncoder {
/// Defines where the URL-encoded string should be set for each `URLRequest`.
public enum Destination {
/// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request.
/// Sets it to the `httpBody` for all other methods.
case methodDependent
/// Applies the encoded query string to any existing query string from the `URLRequest`.
case queryString
/// Applies the encoded query string to the `httpBody` of the `URLRequest`.
case httpBody
/// Determines whether the URL-encoded string should be applied to the `URLRequest`'s `url`.
///
/// - Parameter method: The `HTTPMethod`.
///
/// - Returns: Whether the URL-encoded string should be applied to a `URL`.
func encodesParametersInURL(for method: HTTPMethod) -> Bool {
switch self {
case .methodDependent: return [.get, .head, .delete].contains(method)
case .queryString: return true
case .httpBody: return false
}
}
}
/// Returns an encoder with default parameters.
public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() }
/// The `URLEncodedFormEncoder` to use.
public let encoder: URLEncodedFormEncoder
/// The `Destination` for the URL-encoded string.
public let destination: Destination
/// Creates an instance with the provided `URLEncodedFormEncoder` instance and `Destination` value.
///
/// - Parameters:
/// - encoder: The `URLEncodedFormEncoder`. `URLEncodedFormEncoder()` by default.
/// - destination: The `Destination`. `.methodDependent` by default.
public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) {
self.encoder = encoder
self.destination = destination
}
open func encode<Parameters: Encodable>(_ parameters: Parameters?,
into request: URLRequest) throws -> URLRequest {
guard let parameters else { return request }
var request = request
guard let url = request.url else {
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
}
guard let method = request.method else {
let rawValue = request.method?.rawValue ?? "nil"
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue)))
}
if destination.encodesParametersInURL(for: method),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
let query: String = try Result<String, Error> { try encoder.encode(parameters) }
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get()
let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands()
components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString
guard let newURL = components.url else {
throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
}
request.url = newURL
} else {
if request.headers["Content-Type"] == nil {
request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
}
request.httpBody = try Result<Data, Error> { try encoder.encode(parameters) }
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get()
}
return request
}
}
extension ParameterEncoder where Self == URLEncodedFormParameterEncoder {
/// Provides a default `URLEncodedFormParameterEncoder` instance.
public static var urlEncodedForm: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() }
/// Creates a `URLEncodedFormParameterEncoder` with the provided encoder and destination.
///
/// - Parameters:
/// - encoder: `URLEncodedFormEncoder` used to encode the parameters. `URLEncodedFormEncoder()` by default.
/// - destination: `Destination` to which to encode the parameters. `.methodDependent` by default.
/// - Returns: The `URLEncodedFormParameterEncoder`.
public static func urlEncodedForm(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(),
destination: URLEncodedFormParameterEncoder.Destination = .methodDependent) -> URLEncodedFormParameterEncoder {
URLEncodedFormParameterEncoder(encoder: encoder, destination: destination)
}
}

View File

@ -0,0 +1,349 @@
//
// ParameterEncoding.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A dictionary of parameters to apply to a `URLRequest`.
public typealias Parameters = [String: Any]
/// A type used to define how a set of parameters are applied to a `URLRequest`.
public protocol ParameterEncoding {
/// Creates a `URLRequest` by encoding parameters and applying them on the passed request.
///
/// - Parameters:
/// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded.
/// - parameters: `Parameters` to encode onto the request.
///
/// - Returns: The encoded `URLRequest`.
/// - Throws: Any `Error` produced during parameter encoding.
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
// MARK: -
/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
/// 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`.
///
/// 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
/// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
/// resulting URL request.
public enum Destination {
/// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and
/// sets as the HTTP body for requests with any other HTTP method.
case methodDependent
/// Sets or appends encoded query string result to existing query string.
case queryString
/// Sets encoded query string result as the HTTP body of the URL request.
case httpBody
func encodesParametersInURL(for method: HTTPMethod) -> Bool {
switch self {
case .methodDependent: return [.get, .head, .delete].contains(method)
case .queryString: return true
case .httpBody: return false
}
}
}
/// Configures how `Array` parameters are encoded.
public enum ArrayEncoding {
/// An empty set of square brackets is appended to the key for every value. This is the default behavior.
case brackets
/// No brackets are appended. The key is encoded as is.
case noBrackets
/// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior.
case indexInBrackets
/// Provide a custom array key encoding with the given closure.
case custom((_ key: String, _ index: Int) -> String)
func encode(key: String, atIndex index: Int) -> String {
switch self {
case .brackets:
return "\(key)[]"
case .noBrackets:
return key
case .indexInBrackets:
return "\(key)[\(index)]"
case let .custom(encoding):
return encoding(key, index)
}
}
}
/// Configures how `Bool` parameters are encoded.
public enum BoolEncoding {
/// Encode `true` as `1` and `false` as `0`. This is the default behavior.
case numeric
/// Encode `true` and `false` as string literals.
case 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 with a `.methodDependent` destination.
public static var `default`: URLEncoding { URLEncoding() }
/// Returns a `URLEncoding` instance with a `.queryString` destination.
public static var queryString: URLEncoding { URLEncoding(destination: .queryString) }
/// Returns a `URLEncoding` instance with an `.httpBody` destination.
public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) }
/// 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 an instance using the specified parameters.
///
/// - Parameters:
/// - destination: `Destination` defining where the encoded query string will be applied. `.methodDependent` by
/// default.
/// - arrayEncoding: `ArrayEncoding` to use. `.brackets` by default.
/// - boolEncoding: `BoolEncoding` to use. `.numeric` by default.
public init(destination: Destination = .methodDependent,
arrayEncoding: ArrayEncoding = .brackets,
boolEncoding: BoolEncoding = .numeric) {
self.destination = destination
self.arrayEncoding = arrayEncoding
self.boolEncoding = boolEncoding
}
// MARK: Encoding
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard let parameters else { return urlRequest }
if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
guard let url = urlRequest.url else {
throw AFError.parameterEncodingFailed(reason: .missingURL)
}
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
urlComponents.percentEncodedQuery = percentEncodedQuery
urlRequest.url = urlComponents.url
}
} else {
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
}
urlRequest.httpBody = Data(query(parameters).utf8)
}
return urlRequest
}
/// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively.
///
/// - Parameters:
/// - key: Key of the query component.
/// - value: Value of the query component.
///
/// - Returns: The percent-escaped, URL encoded query string components.
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []
switch value {
case let dictionary as [String: Any]:
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
case let array as [Any]:
for (index, value) in array.enumerated() {
components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value)
}
case let number as NSNumber:
if number.isBool {
components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
} else {
components.append((escape(key), escape("\(number)")))
}
case let bool as Bool:
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
default:
components.append((escape(key), escape("\(value)")))
}
return components
}
/// Creates a percent-escaped string following RFC 3986 for a query string key or value.
///
/// - Parameter string: `String` to be percent-escaped.
///
/// - Returns: The percent-escaped `String`.
public func escape(_ string: String) -> String {
string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
private func query(_ parameters: [String: Any]) -> String {
var components: [(String, String)] = []
for key in parameters.keys.sorted(by: <) {
let value = parameters[key]!
components += queryComponents(fromKey: key, value: value)
}
return components.map { "\($0)=\($1)" }.joined(separator: "&")
}
}
// MARK: -
/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
public struct JSONEncoding: ParameterEncoding {
/// Error produced by `JSONEncoding`.
public enum Error: Swift.Error {
/// `JSONEncoding` attempted to encode an invalid JSON object. Usually this means the value included types not
/// representable in Objective-C. See `JSONSerialization.isValidJSONObject` for details.
case invalidJSONObject
}
// MARK: Properties
/// Returns a `JSONEncoding` instance with default writing options.
public static var `default`: JSONEncoding { JSONEncoding() }
/// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) }
/// The options for writing the parameters as JSON data.
public let options: JSONSerialization.WritingOptions
// MARK: Initialization
/// Creates an instance using the specified `WritingOptions`.
///
/// - Parameter options: `JSONSerialization.WritingOptions` to use.
public init(options: JSONSerialization.WritingOptions = []) {
self.options = options
}
// MARK: Encoding
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard let parameters else { return urlRequest }
guard JSONSerialization.isValidJSONObject(parameters) else {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: Error.invalidJSONObject))
}
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/json"))
}
urlRequest.httpBody = data
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return urlRequest
}
/// Encodes any JSON compatible object into a `URLRequest`.
///
/// - Parameters:
/// - urlRequest: `URLRequestConvertible` value into which the object will be encoded.
/// - jsonObject: `Any` value (must be JSON compatible) to be encoded into the `URLRequest`. `nil` by default.
///
/// - Returns: The encoded `URLRequest`.
/// - Throws: Any `Error` produced during encoding.
public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard let jsonObject else { return urlRequest }
guard JSONSerialization.isValidJSONObject(jsonObject) else {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: Error.invalidJSONObject))
}
do {
let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/json"))
}
urlRequest.httpBody = data
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return urlRequest
}
}
extension JSONEncoding.Error {
public var localizedDescription: String {
"""
Invalid JSON object provided for parameter or object encoding. \
This is most likely due to a value which can't be represented in Objective-C.
"""
}
}
// MARK: -
extension NSNumber {
fileprivate var isBool: Bool {
// Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of
// 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).
String(cString: objCType) == "c"
}
}

168
Pods/Alamofire/Source/Core/Protected.swift generated Normal file
View File

@ -0,0 +1,168 @@
//
// Protected.swift
//
// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
private protocol Lock {
func lock()
func unlock()
}
extension Lock {
/// Executes a closure returning a value while acquiring the lock.
///
/// - Parameter closure: The closure to run.
///
/// - Returns: The value the closure generated.
func around<T>(_ closure: () throws -> T) rethrows -> T {
lock(); defer { unlock() }
return try closure()
}
/// Execute a closure while acquiring the lock.
///
/// - Parameter closure: The closure to run.
func around(_ closure: () throws -> Void) rethrows {
lock(); defer { unlock() }
try closure()
}
}
#if canImport(Darwin)
/// An `os_unfair_lock` wrapper.
final class UnfairLock: Lock {
private let unfairLock: os_unfair_lock_t
init() {
unfairLock = .allocate(capacity: 1)
unfairLock.initialize(to: os_unfair_lock())
}
deinit {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}
fileprivate func lock() {
os_unfair_lock_lock(unfairLock)
}
fileprivate func unlock() {
os_unfair_lock_unlock(unfairLock)
}
}
#elseif canImport(Foundation)
extension NSLock: Lock {}
#else
#error("This platform needs a Lock-conforming type without Foundation.")
#endif
/// A thread-safe wrapper around a value.
@dynamicMemberLookup
final class Protected<Value> {
#if canImport(Darwin)
private let lock = UnfairLock()
#elseif canImport(Foundation)
private let lock = NSLock()
#else
#error("This platform needs a Lock-conforming type without Foundation.")
#endif
private var value: Value
init(_ value: Value) {
self.value = value
}
/// Synchronously read or transform the contained value.
///
/// - Parameter closure: The closure to execute.
///
/// - Returns: The return value of the closure passed.
func read<U>(_ closure: (Value) throws -> U) rethrows -> U {
try lock.around { try closure(self.value) }
}
/// Synchronously modify the protected value.
///
/// - Parameter closure: The closure to execute.
///
/// - Returns: The modified value.
@discardableResult
func write<U>(_ closure: (inout Value) throws -> U) rethrows -> U {
try lock.around { try closure(&self.value) }
}
/// Synchronously update the protected value.
///
/// - Parameter value: The `Value`.
func write(_ value: Value) {
write { $0 = value }
}
subscript<Property>(dynamicMember keyPath: WritableKeyPath<Value, Property>) -> Property {
get { lock.around { value[keyPath: keyPath] } }
set { lock.around { value[keyPath: keyPath] = newValue } }
}
subscript<Property>(dynamicMember keyPath: KeyPath<Value, Property>) -> Property {
lock.around { value[keyPath: keyPath] }
}
}
extension Protected where Value == Request.MutableState {
/// Attempts to transition to the passed `State`.
///
/// - Parameter state: The `State` to attempt transition to.
///
/// - Returns: Whether the transition occurred.
func attemptToTransitionTo(_ state: Request.State) -> Bool {
lock.around {
guard value.state.canTransitionTo(state) else { return false }
value.state = state
return true
}
}
/// Perform a closure while locked with the provided `Request.State`.
///
/// - Parameter perform: The closure to perform while locked.
func withState(perform: (Request.State) -> Void) {
lock.around { perform(value.state) }
}
}
extension Protected: Equatable where Value: Equatable {
static func ==(lhs: Protected<Value>, rhs: Protected<Value>) -> Bool {
lhs.read { left in rhs.read { right in left == right }}
}
}
extension Protected: Hashable where Value: Hashable {
func hash(into hasher: inout Hasher) {
read { hasher.combine($0) }
}
}

1090
Pods/Alamofire/Source/Core/Request.swift generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
//
// RequestTaskMap.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// A type that maintains a two way, one to one map of `URLSessionTask`s to `Request`s.
struct RequestTaskMap {
private typealias Events = (completed: Bool, metricsGathered: Bool)
private var tasksToRequests: [URLSessionTask: Request]
private var requestsToTasks: [Request: URLSessionTask]
private var taskEvents: [URLSessionTask: Events]
var requests: [Request] {
Array(tasksToRequests.values)
}
init(tasksToRequests: [URLSessionTask: Request] = [:],
requestsToTasks: [Request: URLSessionTask] = [:],
taskEvents: [URLSessionTask: (completed: Bool, metricsGathered: Bool)] = [:]) {
self.tasksToRequests = tasksToRequests
self.requestsToTasks = requestsToTasks
self.taskEvents = taskEvents
}
subscript(_ request: Request) -> URLSessionTask? {
get { requestsToTasks[request] }
set {
guard let newValue else {
guard let task = requestsToTasks[request] else {
fatalError("RequestTaskMap consistency error: no task corresponding to request found.")
}
requestsToTasks.removeValue(forKey: request)
tasksToRequests.removeValue(forKey: task)
taskEvents.removeValue(forKey: task)
return
}
requestsToTasks[request] = newValue
tasksToRequests[newValue] = request
taskEvents[newValue] = (completed: false, metricsGathered: false)
}
}
subscript(_ task: URLSessionTask) -> Request? {
get { tasksToRequests[task] }
set {
guard let newValue else {
guard let request = tasksToRequests[task] else {
fatalError("RequestTaskMap consistency error: no request corresponding to task found.")
}
tasksToRequests.removeValue(forKey: task)
requestsToTasks.removeValue(forKey: request)
taskEvents.removeValue(forKey: task)
return
}
tasksToRequests[task] = newValue
requestsToTasks[newValue] = task
taskEvents[task] = (completed: false, metricsGathered: false)
}
}
var count: Int {
precondition(tasksToRequests.count == requestsToTasks.count,
"RequestTaskMap.count invalid, requests.count: \(tasksToRequests.count) != tasks.count: \(requestsToTasks.count)")
return tasksToRequests.count
}
var eventCount: Int {
precondition(taskEvents.count == count, "RequestTaskMap.eventCount invalid, count: \(count) != taskEvents.count: \(taskEvents.count)")
return taskEvents.count
}
var isEmpty: Bool {
precondition(tasksToRequests.isEmpty == requestsToTasks.isEmpty,
"RequestTaskMap.isEmpty invalid, requests.isEmpty: \(tasksToRequests.isEmpty) != tasks.isEmpty: \(requestsToTasks.isEmpty)")
return tasksToRequests.isEmpty
}
var isEventsEmpty: Bool {
precondition(taskEvents.isEmpty == isEmpty, "RequestTaskMap.isEventsEmpty invalid, isEmpty: \(isEmpty) != taskEvents.isEmpty: \(taskEvents.isEmpty)")
return taskEvents.isEmpty
}
mutating func disassociateIfNecessaryAfterGatheringMetricsForTask(_ task: URLSessionTask) -> Bool {
guard let events = taskEvents[task] else {
fatalError("RequestTaskMap consistency error: no events corresponding to task found.")
}
switch (events.completed, events.metricsGathered) {
case (_, true): fatalError("RequestTaskMap consistency error: duplicate metricsGatheredForTask call.")
case (false, false): taskEvents[task] = (completed: false, metricsGathered: true); return false
case (true, false): self[task] = nil; return true
}
}
mutating func disassociateIfNecessaryAfterCompletingTask(_ task: URLSessionTask) -> Bool {
guard let events = taskEvents[task] else {
fatalError("RequestTaskMap consistency error: no events corresponding to task found.")
}
switch (events.completed, events.metricsGathered) {
case (true, _): fatalError("RequestTaskMap consistency error: duplicate completionReceivedForTask call.")
// swift-corelibs-foundation doesn't gather metrics, so unconditionally remove the reference and return true.
#if canImport(FoundationNetworking)
default: self[task] = nil; return true
#else
case (false, false):
if #available(macOS 10.12, iOS 10, watchOS 7, tvOS 10, *) {
taskEvents[task] = (completed: true, metricsGathered: false); return false
} else {
// watchOS < 7 doesn't gather metrics, so unconditionally remove the reference and return true.
self[task] = nil; return true
}
case (false, true):
self[task] = nil; return true
#endif
}
}
}

453
Pods/Alamofire/Source/Core/Response.swift generated Normal file
View File

@ -0,0 +1,453 @@
//
// Response.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Default type of `DataResponse` returned by Alamofire, with an `AFError` `Failure` type.
public typealias AFDataResponse<Success> = DataResponse<Success, AFError>
/// Default type of `DownloadResponse` returned by Alamofire, with an `AFError` `Failure` type.
public typealias AFDownloadResponse<Success> = DownloadResponse<Success, AFError>
/// Type used to store all values associated with a serialized response of a `DataRequest` or `UploadRequest`.
public struct DataResponse<Success, Failure: Error> {
/// The URL request sent to the server.
public let request: URLRequest?
/// The server's response to the URL request.
public let response: HTTPURLResponse?
/// The data returned by the server.
public let data: Data?
/// The final metrics of the response.
///
/// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.`
///
public let metrics: URLSessionTaskMetrics?
/// The time taken to serialize the response.
public let serializationDuration: TimeInterval
/// The result of response serialization.
public let result: Result<Success, Failure>
/// Returns the associated value of the result if it is a success, `nil` otherwise.
public var value: Success? { result.success }
/// Returns the associated error value if the result if it is a failure, `nil` otherwise.
public var error: Failure? { result.failure }
/// Creates a `DataResponse` instance with the specified parameters derived from the response serialization.
///
/// - Parameters:
/// - request: The `URLRequest` sent to the server.
/// - response: The `HTTPURLResponse` from the server.
/// - data: The `Data` returned by the server.
/// - metrics: The `URLSessionTaskMetrics` of the `DataRequest` or `UploadRequest`.
/// - serializationDuration: The duration taken by serialization.
/// - result: The `Result` of response serialization.
public init(request: URLRequest?,
response: HTTPURLResponse?,
data: Data?,
metrics: URLSessionTaskMetrics?,
serializationDuration: TimeInterval,
result: Result<Success, Failure>) {
self.request = request
self.response = response
self.data = data
self.metrics = metrics
self.serializationDuration = serializationDuration
self.result = result
}
}
// MARK: -
extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
"\(result)"
}
/// The debug textual representation used when written to an output stream, which includes (if available) a summary
/// of the `URLRequest`, the request's headers and body (if decodable as a `String` below 100KB); the
/// `HTTPURLResponse`'s status code, headers, and body; the duration of the network and serialization actions; and
/// the `Result` of serialization.
public var debugDescription: String {
guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" }
let requestDescription = DebugDescription.description(of: urlRequest)
let responseDescription = response.map { response in
let responseBodyDescription = DebugDescription.description(for: data, headers: response.headers)
return """
\(DebugDescription.description(of: response))
\(responseBodyDescription.indentingNewlines())
"""
} ?? "[Response]: None"
let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None"
return """
\(requestDescription)
\(responseDescription)
[Network Duration]: \(networkDuration)
[Serialization Duration]: \(serializationDuration)s
[Result]: \(result)
"""
}
}
// MARK: -
extension DataResponse {
/// Evaluates the specified closure when the result of this `DataResponse` is a success, passing the unwrapped
/// result value as a parameter.
///
/// Use the `map` method with a closure that does not throw. For example:
///
/// let possibleData: DataResponse<Data> = ...
/// let possibleInt = possibleData.map { $0.count }
///
/// - parameter transform: A closure that takes the success value of the instance's result.
///
/// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's
/// result is a failure, returns a response wrapping the same failure.
public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> DataResponse<NewSuccess, Failure> {
DataResponse<NewSuccess, Failure>(request: request,
response: response,
data: data,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.map(transform))
}
/// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result
/// value as a parameter.
///
/// Use the `tryMap` method with a closure that may throw an error. For example:
///
/// let possibleData: DataResponse<Data> = ...
/// let possibleObject = possibleData.tryMap {
/// try JSONSerialization.jsonObject(with: $0)
/// }
///
/// - parameter transform: A closure that takes the success value of the instance's result.
///
/// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's
/// result is a failure, returns the same failure.
public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DataResponse<NewSuccess, Error> {
DataResponse<NewSuccess, Error>(request: request,
response: response,
data: data,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.tryMap(transform))
}
/// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
///
/// Use the `mapError` function with a closure that does not throw. For example:
///
/// let possibleData: DataResponse<Data> = ...
/// let withMyError = possibleData.mapError { MyError.error($0) }
///
/// - Parameter transform: A closure that takes the error of the instance.
///
/// - Returns: A `DataResponse` instance containing the result of the transform.
public func mapError<NewFailure: Error>(_ transform: (Failure) -> NewFailure) -> DataResponse<Success, NewFailure> {
DataResponse<Success, NewFailure>(request: request,
response: response,
data: data,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.mapError(transform))
}
/// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
///
/// Use the `tryMapError` function with a closure that may throw an error. For example:
///
/// let possibleData: DataResponse<Data> = ...
/// let possibleObject = possibleData.tryMapError {
/// try someFailableFunction(taking: $0)
/// }
///
/// - Parameter transform: A throwing closure that takes the error of the instance.
///
/// - Returns: A `DataResponse` instance containing the result of the transform.
public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DataResponse<Success, Error> {
DataResponse<Success, Error>(request: request,
response: response,
data: data,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.tryMapError(transform))
}
}
// MARK: -
/// Used to store all data associated with a serialized response of a download request.
public struct DownloadResponse<Success, Failure: Error> {
/// The URL request sent to the server.
public let request: URLRequest?
/// The server's response to the URL request.
public let response: HTTPURLResponse?
/// The final destination URL of the data returned from the server after it is moved.
public let fileURL: URL?
/// The resume data generated if the request was cancelled.
public let resumeData: Data?
/// The final metrics of the response.
///
/// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.`
///
public let metrics: URLSessionTaskMetrics?
/// The time taken to serialize the response.
public let serializationDuration: TimeInterval
/// The result of response serialization.
public let result: Result<Success, Failure>
/// Returns the associated value of the result if it is a success, `nil` otherwise.
public var value: Success? { result.success }
/// Returns the associated error value if the result if it is a failure, `nil` otherwise.
public var error: Failure? { result.failure }
/// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization.
///
/// - Parameters:
/// - request: The `URLRequest` sent to the server.
/// - response: The `HTTPURLResponse` from the server.
/// - fileURL: The final destination URL of the data returned from the server after it is moved.
/// - resumeData: The resume `Data` generated if the request was cancelled.
/// - metrics: The `URLSessionTaskMetrics` of the `DownloadRequest`.
/// - serializationDuration: The duration taken by serialization.
/// - result: The `Result` of response serialization.
public init(request: URLRequest?,
response: HTTPURLResponse?,
fileURL: URL?,
resumeData: Data?,
metrics: URLSessionTaskMetrics?,
serializationDuration: TimeInterval,
result: Result<Success, Failure>) {
self.request = request
self.response = response
self.fileURL = fileURL
self.resumeData = resumeData
self.metrics = metrics
self.serializationDuration = serializationDuration
self.result = result
}
}
// MARK: -
extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
"\(result)"
}
/// The debug textual representation used when written to an output stream, which includes the URL request, the URL
/// response, the temporary and destination URLs, the resume data, the durations of the network and serialization
/// actions, and the response serialization result.
public var debugDescription: String {
guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" }
let requestDescription = DebugDescription.description(of: urlRequest)
let responseDescription = response.map(DebugDescription.description(of:)) ?? "[Response]: None"
let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None"
let resumeDataDescription = resumeData.map { "\($0)" } ?? "None"
return """
\(requestDescription)
\(responseDescription)
[File URL]: \(fileURL?.path ?? "None")
[Resume Data]: \(resumeDataDescription)
[Network Duration]: \(networkDuration)
[Serialization Duration]: \(serializationDuration)s
[Result]: \(result)
"""
}
}
// MARK: -
extension DownloadResponse {
/// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
/// result value as a parameter.
///
/// Use the `map` method with a closure that does not throw. For example:
///
/// let possibleData: DownloadResponse<Data> = ...
/// let possibleInt = possibleData.map { $0.count }
///
/// - parameter transform: A closure that takes the success value of the instance's result.
///
/// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's
/// result is a failure, returns a response wrapping the same failure.
public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> DownloadResponse<NewSuccess, Failure> {
DownloadResponse<NewSuccess, Failure>(request: request,
response: response,
fileURL: fileURL,
resumeData: resumeData,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.map(transform))
}
/// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
/// result value as a parameter.
///
/// Use the `tryMap` method with a closure that may throw an error. For example:
///
/// let possibleData: DownloadResponse<Data> = ...
/// let possibleObject = possibleData.tryMap {
/// try JSONSerialization.jsonObject(with: $0)
/// }
///
/// - parameter transform: A closure that takes the success value of the instance's result.
///
/// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this
/// instance's result is a failure, returns the same failure.
public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DownloadResponse<NewSuccess, Error> {
DownloadResponse<NewSuccess, Error>(request: request,
response: response,
fileURL: fileURL,
resumeData: resumeData,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.tryMap(transform))
}
/// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
///
/// Use the `mapError` function with a closure that does not throw. For example:
///
/// let possibleData: DownloadResponse<Data> = ...
/// let withMyError = possibleData.mapError { MyError.error($0) }
///
/// - Parameter transform: A closure that takes the error of the instance.
///
/// - Returns: A `DownloadResponse` instance containing the result of the transform.
public func mapError<NewFailure: Error>(_ transform: (Failure) -> NewFailure) -> DownloadResponse<Success, NewFailure> {
DownloadResponse<Success, NewFailure>(request: request,
response: response,
fileURL: fileURL,
resumeData: resumeData,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.mapError(transform))
}
/// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
///
/// Use the `tryMapError` function with a closure that may throw an error. For example:
///
/// let possibleData: DownloadResponse<Data> = ...
/// let possibleObject = possibleData.tryMapError {
/// try someFailableFunction(taking: $0)
/// }
///
/// - Parameter transform: A throwing closure that takes the error of the instance.
///
/// - Returns: A `DownloadResponse` instance containing the result of the transform.
public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DownloadResponse<Success, Error> {
DownloadResponse<Success, Error>(request: request,
response: response,
fileURL: fileURL,
resumeData: resumeData,
metrics: metrics,
serializationDuration: serializationDuration,
result: result.tryMapError(transform))
}
}
private enum DebugDescription {
static func description(of request: URLRequest) -> String {
let requestSummary = "\(request.httpMethod!) \(request)"
let requestHeadersDescription = DebugDescription.description(for: request.headers)
let requestBodyDescription = DebugDescription.description(for: request.httpBody, headers: request.headers)
return """
[Request]: \(requestSummary)
\(requestHeadersDescription.indentingNewlines())
\(requestBodyDescription.indentingNewlines())
"""
}
static func description(of response: HTTPURLResponse) -> String {
"""
[Response]:
[Status Code]: \(response.statusCode)
\(DebugDescription.description(for: response.headers).indentingNewlines())
"""
}
static func description(for headers: HTTPHeaders) -> String {
guard !headers.isEmpty else { return "[Headers]: None" }
let headerDescription = "\(headers.sorted())".indentingNewlines()
return """
[Headers]:
\(headerDescription)
"""
}
static func description(for data: Data?,
headers: HTTPHeaders,
allowingPrintableTypes printableTypes: [String] = ["json", "xml", "text"],
maximumLength: Int = 100_000) -> String {
guard let data, !data.isEmpty else { return "[Body]: None" }
guard
data.count <= maximumLength,
printableTypes.compactMap({ headers["Content-Type"]?.contains($0) }).contains(true)
else { return "[Body]: \(data.count) bytes" }
return """
[Body]:
\(String(decoding: data, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
.indentingNewlines())
"""
}
}
extension String {
fileprivate func indentingNewlines(by spaceCount: Int = 4) -> String {
let spaces = String(repeating: " ", count: spaceCount)
return replacingOccurrences(of: "\n", with: "\n\(spaces)")
}
}

1350
Pods/Alamofire/Source/Core/Session.swift generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,387 @@
//
// SessionDelegate.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Class which implements the various `URLSessionDelegate` methods to connect various Alamofire features.
open class SessionDelegate: NSObject {
private let fileManager: FileManager
weak var stateProvider: SessionStateProvider?
var eventMonitor: EventMonitor?
/// Creates an instance from the given `FileManager`.
///
/// - Parameter fileManager: `FileManager` to use for underlying file management, such as moving downloaded files.
/// `.default` by default.
public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}
/// Internal method to find and cast requests while maintaining some integrity checking.
///
/// - Parameters:
/// - task: The `URLSessionTask` for which to find the associated `Request`.
/// - type: The `Request` subclass type to cast any `Request` associate with `task`.
func request<R: Request>(for task: URLSessionTask, as type: R.Type) -> R? {
guard let provider = stateProvider else {
assertionFailure("StateProvider is nil for task \(task.taskIdentifier).")
return nil
}
return provider.request(for: task) as? R
}
}
/// Type which provides various `Session` state values.
protocol SessionStateProvider: AnyObject {
var serverTrustManager: ServerTrustManager? { get }
var redirectHandler: RedirectHandler? { get }
var cachedResponseHandler: CachedResponseHandler? { get }
func request(for task: URLSessionTask) -> Request?
func didGatherMetricsForTask(_ task: URLSessionTask)
func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void)
func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential?
func cancelRequestsForSessionInvalidation(with error: Error?)
}
// MARK: URLSessionDelegate
extension SessionDelegate: URLSessionDelegate {
open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
eventMonitor?.urlSession(session, didBecomeInvalidWithError: error)
stateProvider?.cancelRequestsForSessionInvalidation(with: error)
}
}
// MARK: URLSessionTaskDelegate
extension SessionDelegate: URLSessionTaskDelegate {
/// Result of a `URLAuthenticationChallenge` evaluation.
typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: AFError?)
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
eventMonitor?.urlSession(session, task: task, didReceive: challenge)
let evaluation: ChallengeEvaluation
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM,
NSURLAuthenticationMethodNegotiate:
evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
#if canImport(Security)
case NSURLAuthenticationMethodServerTrust:
evaluation = attemptServerTrustAuthentication(with: challenge)
case NSURLAuthenticationMethodClientCertificate:
evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
#endif
default:
evaluation = (.performDefaultHandling, nil, nil)
}
if let error = evaluation.error {
stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error)
}
completionHandler(evaluation.disposition, evaluation.credential)
}
#if canImport(Security)
/// Evaluates the server trust `URLAuthenticationChallenge` received.
///
/// - Parameter challenge: The `URLAuthenticationChallenge`.
///
/// - Returns: The `ChallengeEvaluation`.
func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation {
let host = challenge.protectionSpace.host
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust
else {
return (.performDefaultHandling, nil, nil)
}
do {
guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else {
return (.performDefaultHandling, nil, nil)
}
try evaluator.evaluate(trust, forHost: host)
return (.useCredential, URLCredential(trust: trust), nil)
} catch {
return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error))))
}
}
#endif
/// Evaluates the credential-based authentication `URLAuthenticationChallenge` received for `task`.
///
/// - Parameters:
/// - challenge: The `URLAuthenticationChallenge`.
/// - task: The `URLSessionTask` which received the challenge.
///
/// - Returns: The `ChallengeEvaluation`.
func attemptCredentialAuthentication(for challenge: URLAuthenticationChallenge,
belongingTo task: URLSessionTask) -> ChallengeEvaluation {
guard challenge.previousFailureCount == 0 else {
return (.rejectProtectionSpace, nil, nil)
}
guard let credential = stateProvider?.credential(for: task, in: challenge.protectionSpace) else {
return (.performDefaultHandling, nil, nil)
}
return (.useCredential, credential, nil)
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
eventMonitor?.urlSession(session,
task: task,
didSendBodyData: bytesSent,
totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
eventMonitor?.urlSession(session, taskNeedsNewBodyStream: task)
guard let request = request(for: task, as: UploadRequest.self) else {
assertionFailure("needNewBodyStream did not find UploadRequest.")
completionHandler(nil)
return
}
completionHandler(request.inputStream())
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
eventMonitor?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request)
if let redirectHandler = stateProvider?.request(for: task)?.redirectHandler ?? stateProvider?.redirectHandler {
redirectHandler.task(task, willBeRedirectedTo: request, for: response, completion: completionHandler)
} else {
completionHandler(request)
}
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
eventMonitor?.urlSession(session, task: task, didFinishCollecting: metrics)
stateProvider?.request(for: task)?.didGatherMetrics(metrics)
stateProvider?.didGatherMetricsForTask(task)
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
// NSLog("URLSession: \(session), task: \(task), didCompleteWithError: \(error)")
eventMonitor?.urlSession(session, task: task, didCompleteWithError: error)
let request = stateProvider?.request(for: task)
stateProvider?.didCompleteTask(task) {
request?.didCompleteTask(task, with: error.map { $0.asAFError(or: .sessionTaskFailed(error: $0)) })
}
}
@available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
eventMonitor?.urlSession(session, taskIsWaitingForConnectivity: task)
}
}
// MARK: URLSessionDataDelegate
extension SessionDelegate: URLSessionDataDelegate {
open func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: response)
guard let response = response as? HTTPURLResponse else { completionHandler(.allow); return }
if let request = request(for: dataTask, as: DataRequest.self) {
request.didReceiveResponse(response, completionHandler: completionHandler)
} else if let request = request(for: dataTask, as: DataStreamRequest.self) {
request.didReceiveResponse(response, completionHandler: completionHandler)
} else {
assertionFailure("dataTask did not find DataRequest or DataStreamRequest in didReceive response")
completionHandler(.allow)
return
}
}
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data)
if let request = request(for: dataTask, as: DataRequest.self) {
request.didReceive(data: data)
} else if let request = request(for: dataTask, as: DataStreamRequest.self) {
request.didReceive(data: data)
} else {
assertionFailure("dataTask did not find DataRequest or DataStreamRequest in didReceive data")
return
}
}
open func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void) {
eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse)
if let handler = stateProvider?.request(for: dataTask)?.cachedResponseHandler ?? stateProvider?.cachedResponseHandler {
handler.dataTask(dataTask, willCacheResponse: proposedResponse, completion: completionHandler)
} else {
completionHandler(proposedResponse)
}
}
}
// MARK: URLSessionWebSocketDelegate
#if canImport(Darwin) && !canImport(FoundationNetworking)
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension SessionDelegate: URLSessionWebSocketDelegate {
open func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
// TODO: Add event monitor method.
// NSLog("URLSession: \(session), webSocketTask: \(webSocketTask), didOpenWithProtocol: \(`protocol` ?? "None")")
guard let request = request(for: webSocketTask, as: WebSocketRequest.self) else {
return
}
request.didConnect(protocol: `protocol`)
}
open func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
// TODO: Add event monitor method.
// NSLog("URLSession: \(session), webSocketTask: \(webSocketTask), didCloseWithCode: \(closeCode.rawValue), reason: \(reason ?? Data())")
guard let request = request(for: webSocketTask, as: WebSocketRequest.self) else {
return
}
// On 2021 OSes and above, empty reason is returned as empty Data rather than nil, so make it nil always.
let reason = (reason?.isEmpty == true) ? nil : reason
request.didDisconnect(closeCode: closeCode, reason: reason)
}
}
#endif
// MARK: URLSessionDownloadDelegate
extension SessionDelegate: URLSessionDownloadDelegate {
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
eventMonitor?.urlSession(session,
downloadTask: downloadTask,
didResumeAtOffset: fileOffset,
expectedTotalBytes: expectedTotalBytes)
guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else {
assertionFailure("downloadTask did not find DownloadRequest.")
return
}
downloadRequest.updateDownloadProgress(bytesWritten: fileOffset,
totalBytesExpectedToWrite: expectedTotalBytes)
}
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
eventMonitor?.urlSession(session,
downloadTask: downloadTask,
didWriteData: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else {
assertionFailure("downloadTask did not find DownloadRequest.")
return
}
downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
guard let request = request(for: downloadTask, as: DownloadRequest.self) else {
assertionFailure("downloadTask did not find DownloadRequest.")
return
}
let (destination, options): (URL, DownloadRequest.Options)
if let response = request.response {
(destination, options) = request.destination(location, response)
} else {
// If there's no response this is likely a local file download, so generate the temporary URL directly.
(destination, options) = (DownloadRequest.defaultDestinationURL(location), [])
}
eventMonitor?.request(request, didCreateDestinationURL: destination)
do {
if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
if options.contains(.createIntermediateDirectories) {
let directory = destination.deletingLastPathComponent()
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
try fileManager.moveItem(at: location, to: destination)
request.didFinishDownloading(using: downloadTask, with: .success(destination))
} catch {
request.didFinishDownloading(using: downloadTask, with: .failure(.downloadedFileMoveFailed(error: error,
source: location,
destination: destination)))
}
}
}

View File

@ -0,0 +1,105 @@
//
// URLConvertible+URLRequestConvertible.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Types adopting the `URLConvertible` protocol can be used to construct `URL`s, which can then be used to construct
/// `URLRequest`s.
public protocol URLConvertible {
/// Returns a `URL` from the conforming instance or throws.
///
/// - Returns: The `URL` created from the instance.
/// - Throws: Any error thrown while creating the `URL`.
func asURL() throws -> URL
}
extension String: URLConvertible {
/// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws.
///
/// - Returns: The `URL` initialized with `self`.
/// - Throws: An `AFError.invalidURL` instance.
public func asURL() throws -> URL {
guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }
return url
}
}
extension URL: URLConvertible {
/// Returns `self`.
public func asURL() throws -> URL { self }
}
extension URLComponents: URLConvertible {
/// Returns a `URL` if the `self`'s `url` is not nil, otherwise throws.
///
/// - Returns: The `URL` from the `url` property.
/// - Throws: An `AFError.invalidURL` instance.
public func asURL() throws -> URL {
guard let url else { throw AFError.invalidURL(url: self) }
return url
}
}
// MARK: -
/// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s.
public protocol URLRequestConvertible {
/// Returns a `URLRequest` or throws if an `Error` was encountered.
///
/// - Returns: A `URLRequest`.
/// - Throws: Any error thrown while constructing the `URLRequest`.
func asURLRequest() throws -> URLRequest
}
extension URLRequestConvertible {
/// The `URLRequest` returned by discarding any `Error` encountered.
public var urlRequest: URLRequest? { try? asURLRequest() }
}
extension URLRequest: URLRequestConvertible {
/// Returns `self`.
public func asURLRequest() throws -> URLRequest { self }
}
// MARK: -
extension URLRequest {
/// Creates an instance with the specified `url`, `method`, and `headers`.
///
/// - Parameters:
/// - url: The `URLConvertible` value.
/// - method: The `HTTPMethod`.
/// - headers: The `HTTPHeaders`, `nil` by default.
/// - Throws: Any error thrown while converting the `URLConvertible` to a `URL`.
public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
let url = try url.asURL()
self.init(url: url)
httpMethod = method.rawValue
allHTTPHeaderFields = headers?.dictionary
}
}

View File

@ -0,0 +1,174 @@
//
// UploadRequest.swift
//
// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`.
public final class UploadRequest: DataRequest {
/// Type describing the origin of the upload, whether `Data`, file, or stream.
public enum Uploadable {
/// Upload from the provided `Data` value.
case data(Data)
/// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be
/// automatically removed once uploaded.
case file(URL, shouldRemove: Bool)
/// Upload from the provided `InputStream`.
case stream(InputStream)
}
// MARK: Initial State
/// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance.
public let upload: UploadableConvertible
/// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written
/// to disk.
public let fileManager: FileManager
// MARK: Mutable State
/// `Uploadable` value used by the instance.
public var uploadable: Uploadable?
/// Creates an `UploadRequest` using the provided parameters.
///
/// - Parameters:
/// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default.
/// - convertible: `UploadConvertible` value used to determine the type of upload to be performed.
/// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed.
/// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets
/// `underlyingQueue`, but can be passed another queue from a `Session`.
/// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions.
/// - interceptor: `RequestInterceptor` used throughout the request lifecycle.
/// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form
/// encoded payloads written to disk.
/// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`.
init(id: UUID = UUID(),
convertible: UploadConvertible,
underlyingQueue: DispatchQueue,
serializationQueue: DispatchQueue,
eventMonitor: EventMonitor?,
interceptor: RequestInterceptor?,
fileManager: FileManager,
delegate: RequestDelegate) {
upload = convertible
self.fileManager = fileManager
super.init(id: id,
convertible: convertible,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
/// Called when the `Uploadable` value has been created from the `UploadConvertible`.
///
/// - Parameter uploadable: The `Uploadable` that was created.
func didCreateUploadable(_ uploadable: Uploadable) {
self.uploadable = uploadable
eventMonitor?.request(self, didCreateUploadable: uploadable)
}
/// Called when the `Uploadable` value could not be created.
///
/// - Parameter error: `AFError` produced by the failure.
func didFailToCreateUploadable(with error: AFError) {
self.error = error
eventMonitor?.request(self, didFailToCreateUploadableWithError: error)
retryOrFinish(error: error)
}
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
guard let uploadable else {
fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.")
}
switch uploadable {
case let .data(data): return session.uploadTask(with: request, from: data)
case let .file(url, _): return session.uploadTask(with: request, fromFile: url)
case .stream: return session.uploadTask(withStreamedRequest: request)
}
}
override func reset() {
// Uploadable must be recreated on every retry.
uploadable = nil
super.reset()
}
/// Produces the `InputStream` from `uploadable`, if it can.
///
/// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash.
///
/// - Returns: The `InputStream`.
func inputStream() -> InputStream {
guard let uploadable else {
fatalError("Attempting to access the input stream but the uploadable doesn't exist.")
}
guard case let .stream(stream) = uploadable else {
fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.")
}
eventMonitor?.request(self, didProvideInputStream: stream)
return stream
}
override public func cleanup() {
defer { super.cleanup() }
guard
let uploadable,
case let .file(url, shouldRemove) = uploadable,
shouldRemove
else { return }
try? fileManager.removeItem(at: url)
}
}
/// A type that can produce an `UploadRequest.Uploadable` value.
public protocol UploadableConvertible {
/// Produces an `UploadRequest.Uploadable` value from the instance.
///
/// - Returns: The `UploadRequest.Uploadable`.
/// - Throws: Any `Error` produced during creation.
func createUploadable() throws -> UploadRequest.Uploadable
}
extension UploadRequest.Uploadable: UploadableConvertible {
public func createUploadable() throws -> UploadRequest.Uploadable {
self
}
}
/// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`.
public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {}

View File

@ -0,0 +1,564 @@
//
// WebSocketRequest.swift
//
// Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#if canImport(Darwin) && !canImport(FoundationNetworking) // Only Apple platforms support URLSessionWebSocketTask.
import Foundation
/// `Request` subclass which manages a WebSocket connection using `URLSessionWebSocketTask`.
///
/// - Note: This type is currently experimental. There will be breaking changes before the final public release,
/// especially around adoption of the typed throws feature in Swift 6. Please report any missing features or
/// bugs to https://github.com/Alamofire/Alamofire/issues.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@_spi(WebSocket) public final class WebSocketRequest: Request {
enum IncomingEvent {
case connected(protocol: String?)
case receivedMessage(URLSessionWebSocketTask.Message)
case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
case completed(Completion)
}
public struct Event<Success, Failure: Error> {
public enum Kind {
case connected(protocol: String?)
case receivedMessage(Success)
case serializerFailed(Failure)
// Only received if the server disconnects or we cancel with code, not if we do a simple cancel or error.
case disconnected(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
case completed(Completion)
}
weak var socket: WebSocketRequest?
public let kind: Kind
public var message: Success? {
guard case let .receivedMessage(message) = kind else { return nil }
return message
}
init(socket: WebSocketRequest, kind: Kind) {
self.socket = socket
self.kind = kind
}
public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) {
socket?.close(sending: closeCode, reason: reason)
}
public func cancel() {
socket?.cancel()
}
public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) {
socket?.sendPing(respondingOn: queue, onResponse: onResponse)
}
}
public struct Completion {
/// Last `URLRequest` issued by the instance.
public let request: URLRequest?
/// Last `HTTPURLResponse` received by the instance.
public let response: HTTPURLResponse?
/// Last `URLSessionTaskMetrics` produced for the instance.
public let metrics: URLSessionTaskMetrics?
/// `AFError` produced for the instance, if any.
public let error: AFError?
}
public struct Configuration {
public static var `default`: Self { Self() }
public static func `protocol`(_ protocol: String) -> Self {
Self(protocol: `protocol`)
}
public static func maximumMessageSize(_ maximumMessageSize: Int) -> Self {
Self(maximumMessageSize: maximumMessageSize)
}
public static func pingInterval(_ pingInterval: TimeInterval) -> Self {
Self(pingInterval: pingInterval)
}
public let `protocol`: String?
public let maximumMessageSize: Int
public let pingInterval: TimeInterval?
init(protocol: String? = nil, maximumMessageSize: Int = 1_048_576, pingInterval: TimeInterval? = nil) {
self.protocol = `protocol`
self.maximumMessageSize = maximumMessageSize
self.pingInterval = pingInterval
}
}
/// Response to a sent ping.
public enum PingResponse {
public struct Pong {
let start: Date
let end: Date
let latency: TimeInterval
}
/// Received a pong with the associated state.
case pong(Pong)
/// Received an error.
case error(Error)
/// Did not send the ping, the request is cancelled or suspended.
case unsent
}
struct SocketMutableState {
var enqueuedSends: [(message: URLSessionWebSocketTask.Message,
queue: DispatchQueue,
completionHandler: (Result<Void, Error>) -> Void)] = []
var handlers: [(queue: DispatchQueue, handler: (_ event: IncomingEvent) -> Void)] = []
var pingTimerItem: DispatchWorkItem?
}
let socketMutableState = Protected(SocketMutableState())
var socket: URLSessionWebSocketTask? {
task as? URLSessionWebSocketTask
}
public let convertible: URLRequestConvertible
public let configuration: Configuration
init(id: UUID = UUID(),
convertible: URLRequestConvertible,
configuration: Configuration,
underlyingQueue: DispatchQueue,
serializationQueue: DispatchQueue,
eventMonitor: EventMonitor?,
interceptor: RequestInterceptor?,
delegate: RequestDelegate) {
self.convertible = convertible
self.configuration = configuration
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
var copiedRequest = request
let task: URLSessionWebSocketTask
if let `protocol` = configuration.protocol {
copiedRequest.headers.update(.websocketProtocol(`protocol`))
task = session.webSocketTask(with: copiedRequest)
} else {
task = session.webSocketTask(with: copiedRequest)
}
task.maximumMessageSize = configuration.maximumMessageSize
return task
}
override func didCreateTask(_ task: URLSessionTask) {
super.didCreateTask(task)
guard let webSocketTask = task as? URLSessionWebSocketTask else {
fatalError("Invalid task of type \(task.self) created for WebSocketRequest.")
}
// TODO: What about the any old tasks? Reset their receive?
listen(to: webSocketTask)
// Empty pending messages.
socketMutableState.write { state in
guard !state.enqueuedSends.isEmpty else { return }
let sends = state.enqueuedSends
self.underlyingQueue.async {
for send in sends {
webSocketTask.send(send.message) { error in
send.queue.async {
send.completionHandler(Result(value: (), error: error))
}
}
}
}
state.enqueuedSends = []
}
}
func didClose() {
dispatchPrecondition(condition: .onQueue(underlyingQueue))
mutableState.write { mutableState in
// Check whether error is cancellation or other websocket closing error.
// If so, remove it.
// Otherwise keep it.
if case let .sessionTaskFailed(error) = mutableState.error, (error as? URLError)?.code == .cancelled {
mutableState.error = nil
}
}
// TODO: Still issue this event?
eventMonitor?.requestDidCancel(self)
}
@discardableResult
public func close(sending closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? = nil) -> Self {
cancelAutomaticPing()
mutableState.write { mutableState in
guard mutableState.state.canTransitionTo(.cancelled) else { return }
mutableState.state = .cancelled
underlyingQueue.async { self.didClose() }
guard let task = mutableState.tasks.last, task.state != .completed else {
underlyingQueue.async { self.finish() }
return
}
// Resume to ensure metrics are gathered.
task.resume()
// Cast from state directly, not the property, otherwise the lock is recursive.
(mutableState.tasks.last as? URLSessionWebSocketTask)?.cancel(with: closeCode, reason: reason)
underlyingQueue.async { self.didCancelTask(task) }
}
return self
}
@discardableResult
override public func cancel() -> Self {
cancelAutomaticPing()
return super.cancel()
}
func didConnect(protocol: String?) {
dispatchPrecondition(condition: .onQueue(underlyingQueue))
socketMutableState.read { state in
// TODO: Capture HTTPURLResponse here too?
for handler in state.handlers {
// Saved handler calls out to serializationQueue immediately, then to handler's queue.
handler.handler(.connected(protocol: `protocol`))
}
}
if let pingInterval = configuration.pingInterval {
startAutomaticPing(every: pingInterval)
}
}
public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) {
guard isResumed else {
queue.async { onResponse(.unsent) }
return
}
let start = Date()
let startTimestamp = ProcessInfo.processInfo.systemUptime
socket?.sendPing { error in
// Calls back on delegate queue / rootQueue / underlyingQueue
if let error {
queue.async {
onResponse(.error(error))
}
// TODO: What to do with failed ping? Configure for failure, auto retry, or stop pinging?
} else {
let end = Date()
let endTimestamp = ProcessInfo.processInfo.systemUptime
let pong = PingResponse.Pong(start: start, end: end, latency: endTimestamp - startTimestamp)
queue.async {
onResponse(.pong(pong))
}
}
}
}
func startAutomaticPing(every pingInterval: TimeInterval) {
socketMutableState.write { mutableState in
guard isResumed else {
// Defer out of lock.
defer { cancelAutomaticPing() }
return
}
let item = DispatchWorkItem { [weak self] in
guard let self, self.isResumed else { return }
self.sendPing(respondingOn: self.underlyingQueue) { response in
guard case .pong = response else { return }
self.startAutomaticPing(every: pingInterval)
}
}
mutableState.pingTimerItem = item
underlyingQueue.asyncAfter(deadline: .now() + pingInterval, execute: item)
}
}
#if swift(>=5.8)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
func startAutomaticPing(every duration: Duration) {
let interval = TimeInterval(duration.components.seconds) + (Double(duration.components.attoseconds) / 1e18)
startAutomaticPing(every: interval)
}
#endif
func cancelAutomaticPing() {
socketMutableState.write { mutableState in
mutableState.pingTimerItem?.cancel()
mutableState.pingTimerItem = nil
}
}
func didDisconnect(closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
dispatchPrecondition(condition: .onQueue(underlyingQueue))
cancelAutomaticPing()
socketMutableState.read { state in
for handler in state.handlers {
// Saved handler calls out to serializationQueue immediately, then to handler's queue.
handler.handler(.disconnected(closeCode: closeCode, reason: reason))
}
}
}
private func listen(to task: URLSessionWebSocketTask) {
// TODO: Do we care about the cycle while receiving?
task.receive { result in
switch result {
case let .success(message):
self.socketMutableState.read { state in
for handler in state.handlers {
// Saved handler calls out to serializationQueue immediately, then to handler's queue.
handler.handler(.receivedMessage(message))
}
}
self.listen(to: task)
case .failure:
// It doesn't seem like any relevant errors are received here, just incorrect garbage, like errors when
// the socket disconnects.
break
}
}
}
@discardableResult
public func streamSerializer<Serializer>(
_ serializer: Serializer,
on queue: DispatchQueue = .main,
handler: @escaping (_ event: Event<Serializer.Output, Serializer.Failure>) -> Void
) -> Self where Serializer: WebSocketMessageSerializer, Serializer.Failure == Error {
forIncomingEvent(on: queue) { incomingEvent in
let event: Event<Serializer.Output, Serializer.Failure>
switch incomingEvent {
case let .connected(`protocol`):
event = .init(socket: self, kind: .connected(protocol: `protocol`))
case let .receivedMessage(message):
do {
let serializedMessage = try serializer.decode(message)
event = .init(socket: self, kind: .receivedMessage(serializedMessage))
} catch {
event = .init(socket: self, kind: .serializerFailed(error))
}
case let .disconnected(closeCode, reason):
event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason))
case let .completed(completion):
event = .init(socket: self, kind: .completed(completion))
}
queue.async { handler(event) }
}
}
@discardableResult
public func streamDecodableEvents<Value>(
_ type: Value.Type = Value.self,
on queue: DispatchQueue = .main,
using decoder: DataDecoder = JSONDecoder(),
handler: @escaping (_ event: Event<Value, Error>) -> Void
) -> Self where Value: Decodable {
streamSerializer(DecodableWebSocketMessageDecoder<Value>(decoder: decoder), on: queue, handler: handler)
}
@discardableResult
public func streamDecodable<Value>(
_ type: Value.Type = Value.self,
on queue: DispatchQueue = .main,
using decoder: DataDecoder = JSONDecoder(),
handler: @escaping (_ value: Value) -> Void
) -> Self where Value: Decodable {
streamDecodableEvents(Value.self, on: queue) { event in
event.message.map(handler)
}
}
@discardableResult
public func streamMessageEvents(
on queue: DispatchQueue = .main,
handler: @escaping (_ event: Event<URLSessionWebSocketTask.Message, Never>) -> Void
) -> Self {
forIncomingEvent(on: queue) { incomingEvent in
let event: Event<URLSessionWebSocketTask.Message, Never>
switch incomingEvent {
case let .connected(`protocol`):
event = .init(socket: self, kind: .connected(protocol: `protocol`))
case let .receivedMessage(message):
event = .init(socket: self, kind: .receivedMessage(message))
case let .disconnected(closeCode, reason):
event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason))
case let .completed(completion):
event = .init(socket: self, kind: .completed(completion))
}
queue.async { handler(event) }
}
}
@discardableResult
public func streamMessages(
on queue: DispatchQueue = .main,
handler: @escaping (_ message: URLSessionWebSocketTask.Message) -> Void
) -> Self {
streamMessageEvents(on: queue) { event in
event.message.map(handler)
}
}
func forIncomingEvent(on queue: DispatchQueue, handler: @escaping (IncomingEvent) -> Void) -> Self {
socketMutableState.write { state in
state.handlers.append((queue: queue, handler: { incomingEvent in
self.serializationQueue.async {
handler(incomingEvent)
}
}))
}
appendResponseSerializer {
self.responseSerializerDidComplete {
self.serializationQueue.async {
handler(.completed(.init(request: self.request,
response: self.response,
metrics: self.metrics,
error: self.error)))
}
}
}
return self
}
public func send(_ message: URLSessionWebSocketTask.Message,
queue: DispatchQueue = .main,
completionHandler: @escaping (Result<Void, Error>) -> Void) {
guard !(isCancelled || isFinished) else { return }
guard let socket else {
// URLSessionWebSocketTask not created yet, enqueue the send.
socketMutableState.write { mutableState in
mutableState.enqueuedSends.append((message, queue, completionHandler))
}
return
}
socket.send(message) { error in
queue.async {
completionHandler(Result(value: (), error: error))
}
}
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol WebSocketMessageSerializer<Output, Failure> {
associatedtype Output
associatedtype Failure: Error = Error
func decode(_ message: URLSessionWebSocketTask.Message) throws -> Output
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension WebSocketMessageSerializer {
public static func json<Value>(
decoding _: Value.Type = Value.self,
using decoder: JSONDecoder = JSONDecoder()
) -> DecodableWebSocketMessageDecoder<Value> where Self == DecodableWebSocketMessageDecoder<Value> {
Self(decoder: decoder)
}
static var passthrough: PassthroughWebSocketMessageDecoder {
PassthroughWebSocketMessageDecoder()
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
struct PassthroughWebSocketMessageDecoder: WebSocketMessageSerializer {
public typealias Failure = Never
public func decode(_ message: URLSessionWebSocketTask.Message) -> URLSessionWebSocketTask.Message {
message
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct DecodableWebSocketMessageDecoder<Value: Decodable>: WebSocketMessageSerializer {
public enum Error: Swift.Error {
case decoding(Swift.Error)
case unknownMessage(description: String)
}
public let decoder: DataDecoder
public init(decoder: DataDecoder) {
self.decoder = decoder
}
public func decode(_ message: URLSessionWebSocketTask.Message) throws -> Value {
let data: Data
switch message {
case let .data(messageData):
data = messageData
case let .string(string):
data = Data(string.utf8)
@unknown default:
throw Error.unknownMessage(description: String(describing: message))
}
do {
return try decoder.decode(Value.self, from: data)
} catch {
throw Error.decoding(error)
}
}
}
#endif