-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add DataPreprocessor API #2903
Add DataPreprocessor API #2903
Changes from 3 commits
84b738d
6d1a440
b53cb2c
d2e3580
973202d
3ccdb11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,18 +64,42 @@ public protocol DownloadResponseSerializerProtocol { | |
|
||
/// A serializer that can handle both data and download responses. | ||
public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { | ||
/// `DataPreprocessor` closure used to prepare incoming `Data` for serialization. | ||
var dataPreprocessor: DataPreprocessor { get } | ||
/// `HTTPMethod`s for which empty response bodies are considered appropriate. | ||
var emptyRequestMethods: Set<HTTPMethod> { get } | ||
/// HTTP response codes for which empty response bodies are considered appropriate. | ||
var emptyResponseCodes: Set<Int> { get } | ||
} | ||
|
||
/// Type used to preprocess `Data` before it handled by a serializer. | ||
public protocol DataPreprocessor { | ||
/// Process `Data` before it's handled by a serializer. | ||
/// - Parameter data: The raw `Data` to process. | ||
func preprocess(_ data: Data) throws -> Data | ||
} | ||
|
||
/// `DataPreprocessor` that returns passed `Data` without any transform. | ||
struct Passthrough: DataPreprocessor { | ||
func preprocess(_ data: Data) throws -> Data { return data } | ||
} | ||
|
||
/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header. | ||
struct XSSI: DataPreprocessor { | ||
func preprocess(_ data: Data) throws -> Data { | ||
return (data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data | ||
} | ||
} | ||
|
||
extension ResponseSerializer { | ||
/// Default `DataPreprocessor` that merely passes `Data` through. | ||
public static var defaultDataPreprocessor: DataPreprocessor { return Passthrough() } | ||
/// Default `HTTPMethod`s for which empty response bodies are considered appropriate. `[.head]` by default. | ||
public static var defaultEmptyRequestMethods: Set<HTTPMethod> { return [.head] } | ||
/// HTTP response codes for which empty response bodies are considered appropriate. `[204, 205]` by default. | ||
public static var defaultEmptyResponseCodes: Set<Int> { return [204, 205] } | ||
|
||
public var dataPreprocessor: DataPreprocessor { return Self.defaultDataPreprocessor } | ||
public var emptyRequestMethods: Set<HTTPMethod> { return Self.defaultEmptyRequestMethods } | ||
public var emptyResponseCodes: Set<Int> { return Self.defaultEmptyResponseCodes } | ||
|
||
|
@@ -267,7 +291,7 @@ extension DownloadRequest { | |
-> Self | ||
{ | ||
appendResponseSerializer { | ||
// Start work that should be on the serilization queue. | ||
// Start work that should be on the serialization queue. | ||
let result = Result<URL?, Error>(value: self.fileURL , error: self.error) | ||
// End work that should be on the serialization queue. | ||
|
||
|
@@ -398,33 +422,37 @@ extension DataRequest { | |
/// request returning `nil` or no data is considered an error. However, if the response is has a status code valid for | ||
/// empty responses (`204`, `205`), then an empty `Data` value is returned. | ||
public final class DataResponseSerializer: ResponseSerializer { | ||
/// HTTP response codes for which empty responses are allowed. | ||
public let dataPreprocessor: DataPreprocessor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does Also, would you expect people to create their own chainable extensions when using these?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could be the next step for this API, but I don't think it will be used much, so I don't think it's valuable as a chainable API. Customizing the existing serializers should be enough for what users need here. |
||
public let emptyResponseCodes: Set<Int> | ||
/// HTTP request methods for which empty responses are allowed. | ||
public let emptyRequestMethods: Set<HTTPMethod> | ||
|
||
/// Creates an instance using the provided values. | ||
/// | ||
/// - Parameters: | ||
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. | ||
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. | ||
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. | ||
public init(emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, | ||
public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, | ||
emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, | ||
emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) { | ||
self.dataPreprocessor = dataPreprocessor | ||
self.emptyResponseCodes = emptyResponseCodes | ||
self.emptyRequestMethods = emptyRequestMethods | ||
} | ||
|
||
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { | ||
guard error == nil else { throw error! } | ||
|
||
guard let data = data, !data.isEmpty else { | ||
guard var data = data, !data.isEmpty else { | ||
guard emptyResponseAllowed(forRequest: request, response: response) else { | ||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) | ||
} | ||
|
||
return Data() | ||
} | ||
|
||
data = try dataPreprocessor.preprocess(data) | ||
|
||
return data | ||
} | ||
} | ||
|
@@ -457,23 +485,25 @@ extension DownloadRequest { | |
/// data is considered an error. However, if the response is has a status code valid for empty responses (`204`, `205`), | ||
/// then an empty `String` is returned. | ||
public final class StringResponseSerializer: ResponseSerializer { | ||
public let dataPreprocessor: DataPreprocessor | ||
/// Optional string encoding used to validate the response. | ||
public let encoding: String.Encoding? | ||
/// HTTP response codes for which empty responses are allowed. | ||
public let emptyResponseCodes: Set<Int> | ||
/// HTTP request methods for which empty responses are allowed. | ||
public let emptyRequestMethods: Set<HTTPMethod> | ||
|
||
/// Creates an instance with the provided values. | ||
/// | ||
/// - Parameters: | ||
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. | ||
/// - encoding: A 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: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. | ||
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. | ||
public init(encoding: String.Encoding? = nil, | ||
public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, | ||
encoding: String.Encoding? = nil, | ||
emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, | ||
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) { | ||
self.dataPreprocessor = dataPreprocessor | ||
self.encoding = encoding | ||
self.emptyResponseCodes = emptyResponseCodes | ||
self.emptyRequestMethods = emptyRequestMethods | ||
|
@@ -482,14 +512,16 @@ public final class StringResponseSerializer: ResponseSerializer { | |
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { | ||
guard error == nil else { throw error! } | ||
|
||
guard let data = data, !data.isEmpty else { | ||
guard var data = data, !data.isEmpty else { | ||
guard emptyResponseAllowed(forRequest: request, response: response) else { | ||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) | ||
} | ||
|
||
return "" | ||
} | ||
|
||
data = try dataPreprocessor.preprocess(data) | ||
|
||
var convertedEncoding = encoding | ||
|
||
if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil { | ||
|
@@ -559,38 +591,42 @@ extension DownloadRequest { | |
/// `nil` or no data is considered an error. However, if the response is has a status code valid for empty responses | ||
/// (`204`, `205`), then an `NSNull` value is returned. | ||
public final class JSONResponseSerializer: ResponseSerializer { | ||
/// `JSONSerialization.ReadingOptions` used when serializing a response. | ||
public let options: JSONSerialization.ReadingOptions | ||
/// HTTP response codes for which empty responses are allowed. | ||
public let dataPreprocessor: DataPreprocessor | ||
public let emptyResponseCodes: Set<Int> | ||
/// HTTP request methods for which empty responses are allowed. | ||
public let emptyRequestMethods: Set<HTTPMethod> | ||
/// `JSONSerialization.ReadingOptions` used when serializing a response. | ||
public let options: JSONSerialization.ReadingOptions | ||
|
||
/// Creates an instance with the provided values. | ||
/// | ||
/// - Parameters: | ||
/// - options: The options to use. `.allowFragments` by default. | ||
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. | ||
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. | ||
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. | ||
public init(options: JSONSerialization.ReadingOptions = .allowFragments, | ||
/// - options: The options to use. `.allowFragments` by default. | ||
public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, | ||
emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes, | ||
emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods) { | ||
self.options = options | ||
emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods, | ||
options: JSONSerialization.ReadingOptions = .allowFragments) { | ||
self.dataPreprocessor = dataPreprocessor | ||
self.emptyResponseCodes = emptyResponseCodes | ||
self.emptyRequestMethods = emptyRequestMethods | ||
self.options = options | ||
} | ||
|
||
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { | ||
guard error == nil else { throw error! } | ||
|
||
guard let data = data, !data.isEmpty else { | ||
guard var data = data, !data.isEmpty else { | ||
guard emptyResponseAllowed(forRequest: request, response: response) else { | ||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) | ||
} | ||
|
||
return NSNull() | ||
} | ||
|
||
data = try dataPreprocessor.preprocess(data) | ||
|
||
do { | ||
return try JSONSerialization.jsonObject(with: data, options: options) | ||
} catch { | ||
|
@@ -687,22 +723,24 @@ extension JSONDecoder: DataDecoder { } | |
/// is considered an error. However, if the response is has a status code valid for empty responses (`204`, `205`), then | ||
/// the `Empty.value` value is returned. | ||
public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer { | ||
public let dataPreprocessor: DataPreprocessor | ||
/// The `DataDecoder` instance used to decode responses. | ||
public let decoder: DataDecoder | ||
/// HTTP response codes for which empty responses are allowed. | ||
public let emptyResponseCodes: Set<Int> | ||
/// HTTP request methods for which empty responses are allowed. | ||
public let emptyRequestMethods: Set<HTTPMethod> | ||
|
||
/// Creates an instance using the values provided. | ||
/// | ||
/// - Parameters: | ||
/// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. | ||
/// - decoder: The `DataDecoder`. `JSONDecoder()` by default. | ||
/// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. | ||
/// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. | ||
public init(decoder: DataDecoder = JSONDecoder(), | ||
public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, | ||
decoder: DataDecoder = JSONDecoder(), | ||
emptyResponseCodes: Set<Int> = DecodableResponseSerializer.defaultEmptyResponseCodes, | ||
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer.defaultEmptyRequestMethods) { | ||
self.dataPreprocessor = dataPreprocessor | ||
self.decoder = decoder | ||
self.emptyResponseCodes = emptyResponseCodes | ||
self.emptyRequestMethods = emptyRequestMethods | ||
|
@@ -711,7 +749,7 @@ public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer | |
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { | ||
guard error == nil else { throw error! } | ||
|
||
guard let data = data, !data.isEmpty else { | ||
guard var data = data, !data.isEmpty else { | ||
guard emptyResponseAllowed(forRequest: request, response: response) else { | ||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) | ||
} | ||
|
@@ -723,6 +761,8 @@ public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer | |
return emptyValue | ||
} | ||
|
||
data = try dataPreprocessor.preprocess(data) | ||
|
||
do { | ||
return try decoder.decode(T.self, from: data) | ||
} catch { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this
XSSI
type is actually being used at all right now. Also, would it make sense to exposePassthrough
as apublic
type? Would it be convenient for clients, or would you just expect those building custom response serializers to useResponseSerializer.defaultDataPreprocessor
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If either of these do get made
public
, I think I'd recommend tacking on a suffix likePreprocessor
orDataPreprocessor
so it's a bit more clear what they are.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made them both public, updated the naming, and added some tests.