Skip to content
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

Merged
merged 6 commits into from
Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 62 additions & 22 deletions Source/ResponseSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Copy link
Member

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 expose Passthrough as a public type? Would it be convenient for clients, or would you just expect those building custom response serializers to use ResponseSerializer.defaultDataPreprocessor?

Copy link
Member

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 like Preprocessor or DataPreprocessor so it's a bit more clear what they are.

Copy link
Contributor Author

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.


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 }

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does dataPreprocessor have a place in the top-level response chainable APIs, or is it such an advanced feature that it only warrants customization at the response serializer initializers?

Also, would you expect people to create their own chainable extensions when using these?

Not that this matters so much for this PR, but just curious in general.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down
Loading