Swift 4 generics with struct - swift

I'm trying to use generics with Codable protocol, but I'm getting an error.
Cannot invoke 'decode' with an argument list of type '([T.Type], from: Data)
static func getRequest<T>(model: T.Type, url: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, data: #escaping (Any?, Any?, Error?) -> ()) -> Alamofire.DataRequest {
return Alamofire.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers)
.validate(contentType: [RequestHelper.HeaderKeys.contentTypeJson])
.responseJSON { (response) in
print(response)
switch response.result {
case .success:
let responseData = response.data!
do {
print(model)
print(T.self)
let decodable = try JSONDecoder().decode([model].self, from: responseData)
data(response.response?.allHeaderFields, decodable, nil)
} catch let error {
data(nil, nil, error)
}
case .failure(let requestError):
data(nil, nil, requestError)
print(requestError)
}
}
}
I need pass my struct model to this method
How can I fix this? Does anyone could help me?

decode() can only take a type that is Decodable. You need to specify that in your method signature. Either add where T: Decodable at the end of getRequest's declaration or just put <T: Decodable> inside the brackets to restrict T to decodable types only, and then you should be able to pass your parameter to decode().
EDIT: Looking at your code, there's another error:
let decodable = try JSONDecoder().decode([model].self, from: responseData)
Instead of [model].self, you need to pass [T].self. Otherwise you're passing an array of types rather than the type of an array.

Related

Alamofire, HTTPheaders for post request [string:any]

I need to send a post request using alamofire to my server, one of the header to be sent is not a string value but is an Int
Reading the documentation of Alamofire look like the HTTPHeaders is only type [String: String]
Is there any way to customise the HTTPHeaders to [String:Any]?
I can't find to much understandable for me online.
thanks
Alamofire doesn't have such methods, but you can easily do it
["hey": 1].mapValues { String(describing: $0) } returns [String: String]
If you have many places where you're using it, you can:
Create extension for Dictionary
extension Dictionary where Key == String, Value == Any {
func toHTTPHeaders() -> HTTPHeaders {
HTTPHeaders(mapValues { String(describing: $0) })
}
}
// Usage
AF.request(URL(fileURLWithPath: ""), headers: ["": 1].toHTTPHeaders())
Create extension for HTTPHeaders
extension HTTPHeaders: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, Any)...) {
self.init()
elements.forEach { update(name: $0.0, value: String(describing: $0.1)) }
}
}
// usage
AF.request(URL(fileURLWithPath: ""), headers: HTTPHeaders(["": 1]))
Create extension for Session
extension Session {
open func request(_ convertible: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: [String: Any],
interceptor: RequestInterceptor? = nil,
requestModifier: RequestModifier? = nil) -> DataRequest {
return request(convertible, method: method, parameters: parameters, encoding: encoding, headers: headers.mapValues { String(describing: $0) }, interceptor: interceptor, requestModifier: requestModifier)
}
}
// Usage
AF.request(URL(fileURLWithPath: ""), headers: ["": 1])
The reason there's no such option in Alamofire is type safety. When you use Any you can literary pass any value there and so probability of a mistake is much more. By requiring string library makes sure you're converting all values you need by yourself.
I'd go for the first variant, because it's more clear when you read the code that there's something going on there

Swift Combine: Cannot refactor repetitive code

My API returns this format, where data can contain all kinds of responses.
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
I have made a Codable Response type with a generic for the optional data, of which we do not know the type.
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
I am now trying to write API convenience methods as concisely as possible. So I have a function to return a generic publisher that I can use for all these responses, i.e. one that pre-parses the response and catches any errors upfront.
First, I get a dataTaskPublisher that processes the parameter inputs, if any. Endpoint is just a convenience String enum for my endpoints, Method is similar. MyRequest returns a URLRequest with some necessary headers etc.
Notice the way I define the parameters: params: [String:T]. This is standard JSON so it could be strings, numbers etc.
It seems this T is the problem somehow..
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST && params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
Next, I am parsing the response.
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(\.data)
.mapError { MyError.network(description: "\($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "\($0)")} //(2)
.tryMap {
if $0.status > 204 {
throw MyError.network(description: "\($0.status): \($0.error!)")
}
else {
return $0 // returns a MyResponse
}
}
.mapError { $0 as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "\(err)"))
.eraseToAnyPublisher()
}
}
Now I can write an endpoint method easily. Here are two examples.
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
and
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
All this is working well. Please note that each time, I have to specify my exact return type twice. I think I can live with that.
I should be able to simplify this so that I do not have to repeat the same three operators (map, mapError, receive) in exactly the same way each time.
But when I insert .map(\.data!) at the location //(1) above I get the error Generic parameter T could not be inferred. at the location //(2).
This is really confusing. Why does the generic type in the input parameters play any role here? This must be related to the call to the .decode operator just above, where the generic in question is called R, not T.
Can you explain this? How can I refactor these operators upstream?
This code has a number of small problems. You're right that one is [String: T]. That means that for a given set of parameters, all the values must be of the same type. That's not "JSON." This will accept a [String: String] or a [String: Int], but you can't have both Int and String values in the same dictionary if you do it this way. And it will also accept [String: Document], and it doesn't seem like you really want that.
I'd recommend switching this to just Encodable, which would allow you to pass structs if that were convenient, or Dictionaries if that were convenient:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
Then modify your params.count to check for nil instead.
Note that I didn't make params = nil a default parameter. That's because this would recreate a second problem you have. T (and Params) can't be inferred in the default case. For = [:], what is T? Swift has to know, even though it's empty. So instead of a default, you use an overload:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
Now, when you don't pass any parameters, Params automatically becomes String.
So now your code is fine, and you don't need the as at the end
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
Now, that .map(\.data!) makes me sad. If you get back corrupt data from the server, the app will crash. There are lots of good reasons to crash apps; bad server data is never one of them. But fixing that isn't really related to this question (and is a little bit complicated because Failure types other than Error make things hard currently), so I'll leave it for now. My general recommendation is to use Error as your Failure type, and allow unexpected errors to just bubble up rather than wrapping them in an .undefined case. If you need some catch-all "other" anyway, you might as well do that with types ("is") rather than an extra enum case (which just moves the "is" to a switch). At the very least, I would move the Error->MyError mapping as late as possible, which will make handling this much easier.
One more tweak to make later things a little more general, I suspect MyResponse only needs to be Decodable, not Encodable (the rest of this works either way, but it makes it a little more flexible):
struct MyResponse<T: Decodable>: Decodable { ... }
And to your original question of how to make this reusable, you can now pull out a generic function:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}

How to post simple string (not dictionary) in alamofire?

I have to post raw string at body.
In the usual case, I did this.
let parameters = ["asdf": "asdf", "fdsa", "fdsa"]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON.......
but how to post raw string? (it's not json string. just string.)
AF.request(url, method: .post, parameters: "jsut simple string", encoding: JSONEncoding.default).responseJSON.......
How can I do that??
Alamofire 5 now supports Encodable types as parameters. If you want to encode just a String, switch to using that form of making a Request:
AF.request(url, method: .post, parameters: "just simple string", encoder: JSONParameterEncoder.default)
Note the encoder instead of encoding parameter name and new JSONParameterEncoder type.
Also, using responeJSON is no longer recommended, using responseDecodable to produce Decodable types would be a better approach.
You can use below custom encoding to send single value in param, and pass [:] empty dictionary in parameter
struct SingleValueEncoding: ParameterEncoding {
private let value: String
init(value: String) {
self.value = value
}
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = urlRequest.urlRequest
let data = value.data(using: .utf8)!
if urlRequest?.value(forHTTPHeaderField: "Content-Type") == nil {
urlRequest?.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
urlRequest?.httpBody = data
return urlRequest!
}
}
In Alamofire request, the Parameters is dictionary
/// A dictionary of parameters to apply to a `URLRequest`.
public typealias Parameters = [String: Any]

Convert DataResponse<Data> to expected type DataResponse<Any>?

I have a protocol for handling my requests like this:
protocol NetworkResponseProcessor {
var completionBlock : NetworkRequestCompletionBlock {get set}
init(completionBlock : #escaping NetworkRequestCompletionBlock)
func processResponse(response : DataResponse<Any>?)
func processError(error : Error?)
}
In one of my upload requests using Alamofire I want to pass the response to the processResponse method. However, the response is of type DataResponse<Data> and now Xcode tells me it cannot convert this type to the expected argument type DataReponse<Any>?.
My call looks like this:
Network.shared.upload(multipartFormData: { (multipartFormData) in
multipartFormData.append(fileURL, withName: "content")
},
usingThreshold: UInt64.init(),
to: endpoint,
method: method,
headers: headers) { (encodingResult) in
switch encodingResult {
case .success(let upload, _, _):
upload.validate().responseData(completionHandler: { (response) in
self.responseProcessor.processResponse(response: response)
})
case .failure(let encodingError):
print(encodingError)
self.responseProcessor.processError(error: NetworkRequestError.InvalidJSONData)
}
}
In this code Network is a singleton using shared as SessionManager.
If I use responseJSON, instead of responseData, Xcode is happy, but since the response is binary data, I can't use that method for obvious reasons.
How should I solve the error Xcode is giving me?

Catch pattern changes callback signature

I am trying to use JSONDecoder to decode a json response from my server using Alamofire. When I decode the response with a guard, it works without any issues. The side-effect of this approach is that I can't tell what the issue is when the decode actually fails.
guard let result: TResponseData = try? decoder.decode(TResponseData.self, from: response.data!) else {
self.logger.error("Unable to decode the response data into a model representation.")
return
}
So instead I'm wanting to use a do { } catch { } but I can't figure out how exactly I'm supposed to use it within the Alamofire responseJSON callback.
This is what I've currently got:
Alamofire.request(completeUrl, method: .post, parameters: parameters, encoding: encoding, headers: headers)
.validate()
.responseJSON { (response) -> Void in
self.logger.info("POST Response: \(String(describing:response.response?.statusCode))")
switch response.result {
case .success(_):
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)
do {
let _ = try decoder.decode(TResponseData.self, from: response.data!)
} catch DecodingError.dataCorrupted(let error) {
self.logger.error(error.underlyingError)
return
}
completion(result)
return
case .failure(let error):
//....
}
What I am given with this code however is a compiler error on the .responseJSON { (response) -> Void in line.
Invalid conversion from throwing function of type '(_) -> Void' to non-throwing function type '(DataResponse) -> Void'.
The guard code works fine, and if I change the try to a try? or force an unwrap, it compiles - I just don't get to have my catch handle the actual error.
If I change the catch block so that it does not include any pattern, then the code compiles.
catch {
return
}
This doesn't give me anything over what my guard was giving me. I really want to capture the error encountered with the decode operation. Am I using the wrong pattern? Why does using the DecodingError.dataCorrupted pattern seemingly change the callback signature?
JSONDecoder can throw errors other than DecodingError.dataCorrupted; you need to be able to handle the case of an arbitrary Error being thrown. So, if you want to handle that error, you'll want an unconditional catch {} block.
You can also:
Use responseData instead of responseJSON as you're doing your own deserialisation with JSONDecoder.
Use the unwrap() method on Alamofire's Result type in order to coalesce the network error with the decoding error, if desired.
This is what that looks like:
Alamofire
.request(
completeUrl, method: .post, parameters: parameters,
encoding: encoding, headers: headers
)
.validate()
.responseData { response in
self.logger.info(
"POST Response: \(response.response?.statusCode as Any)"
)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)
do {
let result = try decoder.decode(
TResponseData.self, from: response.result.unwrap()
)
completion(result)
} catch {
self.logger.error(error)
}
}
Although one thing to note here is that you're not calling completion if the request fails; I would personally change that such that you do, and propagate the error back by having the completion take a Result<TResponseData> parameter.
In that case, you can use Result's flatMap(_:) method rather than unwrap() and a catch {} block:
func doRequest(_ completion: #escaping (Result<TResponseData>) -> Void) {
let completeURL = // ...
let parameters = // ...
let encoding = // ...
let headers = // ...
Alamofire
.request(
completeURL, method: .post, parameters: parameters,
encoding: encoding, headers: headers
)
.validate()
.responseData { response in
self.logger.info(
"POST Response: \(response.response?.statusCode as Any)"
)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)
// if response.result is successful data, try to decode.
// if decoding fails, result is that error.
// if response.result is failure, result is that error.
let result = response.result.flatMap {
try decoder.decode(TResponseData.self, from: $0)
}
.ifFailure {
self.logger.error($0)
}
completion(result)
}
}