I'm attempting to define a custom model serialization for Alamofire 4.0. So far I'm following the model presented used by responseJson and friends. Specifically, what I have so far is:
extension Alamofire.Request {
public static func serializeResponseModel<T:ModelObject>(response:HTTPURLResponse?, data:Data?, error:Error?) -> Alamofire.Result<T> {
switch serializeResponseJSON(options: [], response: response, data: data, error: error) {
case .success(let jsonObject):
do {
return .success(try T(json:jsonObject as! JSONObject))
}
catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
}
extension Alamofire.DataRequest {
public static func serializeResponseModel<T:ModelObject>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
return Request.serializeResponseConcierge(response: response, data: data, error: error)
}
}
#discardableResult
public func responseModel<T:ModelObject>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.serializeResponseModel(),
completionHandler: completionHandler
)
}
}
Unfortunately, the framework is somewhat poorly implemented and the line return response( is finding the response property (defined in Request) and not the appropriate response method (defined in DataRequest), which leads to the compile error:
Cannot call value of non-function type 'HTTPURLResponse?'
What am I missing here that allows this to work in the responseJson case, but not in my case?
Apparently the problem arose from over-generalization, and the compiler not being able to generate an appropriate type for DataRequest.serializeResponseModel() When I changed responseModel to the following and specified the appropriate type, things work as expected:
#discardableResult
public func responseModel<T:ModelObject>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.modelResponseSerializer() as DataResponseSerializer<T>,
completionHandler: completionHandler
)
}
Related
I'm trying to migrate my project from Alamofire 4.9 to 5.3 and I'm having a hard time with error handling. I would like to use Decodable as much as possible, but my API endpoints return one JSON structure when everything goes well, and a different JSON structure when there is an error, the same for all errors across all endpoints. The corresponding Codable in my code is ApiError.
I would like to create a custom response serializer that can give me a Result<T, ApiError> instead of the default Result<T, AFError>. I found this article that seems to explain the general process but the code in there does not compile.
How can I create such a custom ResponseSerializer?
I ended up making it work with the following ResponseSerializer:
struct APIError: Error, Decodable {
let message: String
let code: String
let args: [String]
}
final class TwoDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
private lazy var errorSerializer = DecodableResponseSerializer<APIError>(decoder: decoder)
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, APIError> {
guard error == nil else { return .failure(APIError(message: "Unknown error", code: "unknown", args: [])) }
guard let response = response else { return .failure(APIError(message: "Empty response", code: "empty_response", args: [])) }
do {
if response.statusCode < 200 || response.statusCode >= 300 {
let result = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
return .failure(result)
} else {
let result = try successSerializer.serialize(request: request, response: response, data: data, error: nil)
return .success(result)
}
} catch(let err) {
return .failure(APIError(message: "Could not serialize body", code: "unserializable_body", args: [String(data: data!, encoding: .utf8)!, err.localizedDescription]))
}
}
}
extension DataRequest {
#discardableResult func responseTwoDecodable<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: #escaping (Result<T, APIError>) -> Void) -> Self {
return response(queue: .main, responseSerializer: TwoDecodableResponseSerializer<T>()) { response in
switch response.result {
case .success(let result):
completionHandler(result)
case .failure(let error):
completionHandler(.failure(APIError(message: "Other error", code: "other", args: [error.localizedDescription])))
}
}
}
}
And with that, I can call my API like so:
AF.request(request).validate().responseTwoDecodable(of: [Item].self) { response in
switch response {
case .success(let items):
completion(.success(items))
case .failure(let error): //error is an APIError
log.error("Error while loading items: \(String(describing: error))")
completion(.failure(.couldNotLoad(underlyingError: error)))
}
}
I simply consider that any status code outside of the 200-299 range corresponds to an error.
ResponseSerializers have a single requirement. Largely you can just copy the existing serializers. For example, if you wanted to parse a CSV (with no response checking):
struct CommaDelimitedSerializer: ResponseSerializer {
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> [String] {
// Call the existing StringResponseSerializer to get many behaviors automatically.
let string = try StringResponseSerializer().serialize(request: request,
response: response,
data: data,
error: error)
return Array(string.split(separator: ","))
}
}
You can read more in Alamofire's documentation.
In swift how do I throw an error within a completion handler like this:
let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {
(data, response, error) in
do {
//something
completion(result)
} catch let jsonError {
throw CustomError.myerror //THIS DOESN'T WORK
}
})
task.resume()
as the error is
Invalid conversion from throwing function of type '(_, _, _) throws ->
()' to non-throwing function type '(Data?, URLResponse?, Error?) ->
Void'
Short story: You can't throw in a dataTask completion closure
You could return two values in the completion handler
...completion: #escaping (ResultType?, Error?)->Void
and return
completion(result, nil)
completion(nil, CustomError.myerror)
or more convenient use an enum with associated type
enum Result {
case success(ResultType), failure(Error)
}
...completion: #escaping (Result)->Void
and return
completion(.success(result))
completion(.failure(CustomError.myerror))
You can process the result
foo() { result in
switch result {
case .success(let resultType): // do something with the result
case .failure(let error): // Handle the error
}
}
Update:
In Swift 5 using the new built-in Result type it's even more comfortable because Result can capture the result of the throwing expression
...completion: #escaping (Result<MyType,Error>)->Void
let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {
(data, response, error) in
completion(Result { try something()})
})
task.resume()
Update 2:
With async/await completion handlers are gone
do {
let (data, response) = try await URLSession.shared.data(for: request)
} catch {
throw CustomError.myerror
}
I'm in process of unittesting a case where the completionHandler is throwing an error. But I'm not sure how to raise that error.
class MockErrorSession: URLSessionProtocol {
var nextDataTask = MockURLSessionDataTask()
var nextData: Data?
var nextError: Error?
func dataTask(with request: NSURLRequest, completionHandler: #escaping DataTaskResult) -> URLSessionDataTaskProtocol {
nextError = ?
completionHandler(nextData, successHttpURLResponse(request: request), nextError)
return nextDataTask as URLSessionDataTaskProtocol
}
}
I need somehow to populate nextError
I tried to do this,
enum MyError : Error {
case RuntimeError(String)
}
func throwError(_ message: String) throws {
throw MyError.RuntimeError(message)
}
nextError = try throwError("test") as! Error
Any advice please?
Your function dataTask(with:completionHandler:) does not seem to be marked with throws/rethrows in which case it seems the error is simply being passed to the completion block.
Since neither the function nor the closure "throws", you can't throw the error & as a result neither will you be able to do-try-catch it later anyways.
So as per my assumptions, just the following should suffice:
nextError = MyError.RuntimeError("test")
i.e.
enum MyError : Error {
case RuntimeError(String)
}
func dataTask(with request: NSURLRequest,
completionHandler: #escaping DataTaskResult) -> URLSessionDataTaskProtocol {
nextError = MyError.RuntimeError("test")
completionHandler(nextData,
successHttpURLResponse(request: request),
nextError)
return nextDataTask as URLSessionDataTaskProtocol
}
I have an interesting compile error when using one generic to call another. This post is a bit long but hopefully, it's not longer than needed to describe the problem.
I have defined a generic func that works great. I use it a lot and the pattern of use is often the same. I was trying to implement a new generic func that nests the existing generic func, but I'm getting a compile-time error.
For some context, here is how my API works with the generic now. My REST API makes a call the films() func, which hits the StarWarsAPI (swapi.co) and returns a list of all the Star Wars films they have in their database, as follows:
StarWarsAPI.shared.films(){ (films, error) in
for film in films {
print(film.title)
}
}
The films() function calls a generic (restCall()) which works great. Here is the definition of films():
public func films(completion: #escaping (_ films:[Film]?, _ error:StarWarsError?) -> Void) {
guard StarWarsAPI.isOperational else {
return completion(nil,StarWarsError.starWarsAPINotOperational)
}
restCall(fetchUrl: filmsUrl!, modelType: FilmResult()) { (filmResults, error ) in
completion(filmResults?.results, error)
}
}
Where the restCall (the generic) is defined as follows: (Note I'm using the Swift 4 Codable API)
public func restCall<T: Codable>(fetchUrl: URL, modelType: T, completion: #escaping (_ modelObject: T?, _ error:StarWarsError?) -> Void){
var fetchRequest = URLRequest(url: fetchUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
fetchRequest.httpMethod = "GET"
fetchRequest.allHTTPHeaderFields = [
"content-type": "application/json",
"cache-control": "no-cache",
]
let session = URLSession.shared
let fetchDataTask = session.dataTask(with: fetchRequest) { (data, response, error) in
DispatchQueue.main.async { // return to main thread
var modelObject:T?
do {
let jsonDecoder = JSONDecoder()
modelObject = try jsonDecoder.decode(T.self, from: data)// FIXME: Something about decoding the People object is going wrong.
return completion(modelObject, nil)
}catch let error as NSError {
completion(nil, StarWarsError.decodingModelError(error: error))
}
}
}
fetchDataTask.resume()
}
So the above works great. I use it for rest functions films(), people(), ships(), etc. I use the same pattern for each rest call. I want to create a generic that I can use instead of explicit films(), people(), etc.
I've been trying to get the following to work with little success:
public func fetchAll<T: Result>(result:T, completionAll: #escaping (_ result:T?, _ error:StarWarsError?) -> Void) {
restCall(fetchUrl: result.urlPath!, modelType: T) { (finalResults, error ) in
completionAll(finalResults!, error)
}
}
Where Result type is the base type and is defined as follows:
public class Result {
var urlPath:URL?
}
public class FilmResult: Result, Codable {
var count:Int?
var next:String?
var previous:String?
var results:[Film]?
}
The error I'm getting is shown in the screenshot below - hopefully, it's clear.
Any help you can provide would be much appreciated!
Your call has to be
this was T previously ───┐
restCall(fetchUrl: result.urlPath!, modelType: result) { (finalResults, error ) in
Note the result instead of T.
Relatively minimal code to reproduce:
public func restCall<T>(fetchUrl: URL, modelType: T, completion: #escaping (_ modelObject: T?, _ error:String?) -> Void) { }
public func fetchAll<T>(result:T, completionAll: #escaping (_ result:T?, _ error:String?) -> Void) {
┌── should be result
restCall(fetchUrl: URL(string: "asdasd")!, modelType: T) { (finalResults, error ) in
completionAll(finalResults, error)
}
}
I'm trying to make a generic request handler class based on Moya and Object Mapper.
Basically I want a method that performs a request to a Moya Target and performs some basic handling on both sucess and failure cases:
my class would be something like this based on this answer:
class APIRequestHandler<T: Mappable> {
let provider = APIManager().getAPIProvider() // Moya provider
func performRequest(target: Target, completionHandler: (response: T?, error: StoreError?) -> Void) {
provider.request(target) { result in
switch result {
case let .Success(response):
switch response.statusCode {
case 200..<300:
do {
let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .MutableContainers)
let contractualDocument = Mapper<T>().map(json)
completionHandler(response: contractualDocument!, error: nil)
} catch _ {
// Unexpected error when the response can't be parsed
completionHandler(response: nil, error: StoreError.UnexpectedError)
}
case 500:
completionHandler(response: nil, error: StoreError.ServerError)
default:
completionHandler(response: nil, error: StoreError.HTTPError(statusCode: response.statusCode))
}
case .Failure(_):
completionHandler(response: nil, error: StoreError.NetworkError)
}
}
}
}
My question is if I can get the same results using generics at a function level instead of class level:
func performRequest<T: Mappable>(target: Target, completionHandler: (response: T?, error: StoreError?) -> Void) {
provider.request(target) { result in
switch result {
case let .Success(response):
// blah, blah
case .Failure(_):
// bleh, bleh
}
}
}
I've tried it and I get a build error on my code:
Generic parameter "T" could not be inferred