I have multiple classes that have code that calls a common network class to make a GET api call. Below is an example of one:
public typealias Api1Result = (Result<Api1Model>) -> Void
private var path = "the/path/api1"
public enum Api1ServiceError: String, Error {
case error = "Sorry, the api1 service returned something different than expected"
}
extension Api1Model {
public static func getApi1(networkClient: NetworkClient = networkClient, completion: #escaping Api1Result) {
networkClient.getPath(path) { result in
switch result {
case .success(let data):
do {
let api1Model = try JSONDecoder().decode(Api1Model.self, from: data)
completion(.success(api1Model))
} catch {
completion(.failure(Api1ServiceError.error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
Here is the Result enum if interested:
public enum Result<Value> {
case success(Value)
case failure(Error)
}
There are several other model classes, and the only difference is the actual model class being decoded (Api1Model in this case), as well as the completion typealias (Api1Result). It does the exact same thing across several others, just makes the call to the networkClient.getPath() method, checks for success/failure, and calls the completion closure.
Curious if there are any protocol experts out there who could assist in simplifying this and refactoring so I don't have the same boiler-plate code across multiple classes?
Use a protocol extension (untested)
protocol ApiModel {
associatedtype ApiType : Decodable = Self
static var path : String { get }
static func getApi1(networkClient: NetworkClient, completion: #escaping (Result<ApiType>) -> Void)
}
extension ApiModel where Self : Decodable {
static func getApi1(networkClient: NetworkClient, completion: #escaping (Result<ApiType>) -> Void) {
networkClient.getPath(path) { result in
switch result {
case .success(let data):
do {
let api1Model = try JSONDecoder().decode(ApiType.self, from: data)
completion(.success(api1Model))
} catch {
completion(.failure(Api1ServiceError.error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
Make all your classes conform to ApiModel and add the static path property. The type alias is going to be inferred.
Related
I try to create a class implementation skeleton and a test for it. I have already had the function setResult and test testSetResult. Could I have something similar for the function setPublisher and testSetPublisher?
class FakeClass {
// ...
public func setResult() -> Result<[String: Any], Error> {
return .success([:])
}
public func setPublisher() -> any Publisher<[String, Any], Error> {
// what should I return? return PassthroughSubject()
}
}
class FakeClassTest: XCTestCase {
// ...
var fakeClass = DependencyInjectionFakeClass() // some dependency injection
func testSetResult() throws {
let result = fakeClass.setResult()
switch result {
case .success(let response):
XCTAsserture(response.isEmpty)
case .failure:
XCTFail()
}
// just a simple fake test
// How could I implement it or do we have sth similar with the function `testSetResult()`
func testSetPublisher() throws {
}
}
I'm using Apollo v0.49.0. It's a library for calling graphQL endpoints, and the way it does this is by generating code before you compile your code.
Before I talk about the generated code, I'd like to talk about what the generated code implements. For this question, it's the GraphQLMutation that's relevant. Here's what it looks like:
public enum GraphQLOperationType {
case query
case mutation
case subscription
}
public protocol GraphQLOperation: AnyObject {
var operationType: GraphQLOperationType { get }
var operationDefinition: String { get }
var operationIdentifier: String? { get }
var operationName: String { get }
var queryDocument: String { get }
var variables: GraphQLMap? { get }
associatedtype Data: GraphQLSelectionSet
}
public extension GraphQLOperation {
var queryDocument: String {
return operationDefinition
}
var operationIdentifier: String? {
return nil
}
var variables: GraphQLMap? {
return nil
}
}
public protocol GraphQLQuery: GraphQLOperation {}
public extension GraphQLQuery {
var operationType: GraphQLOperationType { return .query }
}
public protocol GraphQLMutation: GraphQLOperation {}
public extension GraphQLMutation {
var operationType: GraphQLOperationType { return .mutation }
}
This is 80% of the file; the last 20% is irrelevant IMHO. Note how GraphQLMutation implements GraphQLOperation and the latter has an associatedtype.
The library generates classes based on your graphql server endpoints. Here's what they look like:
public final class ConcreteMutation: GraphQLMutation {
...
public struct Data: GraphQLSelectionSet {
...
}
...
}
As far as I know (I'm new to Swift), I have no control over any of the code I've mentioned so far (other than forking the repo and modifying it). I could change them locally, but they would just be overridden every time they were regenerated.
To use any of these generated classes, I have to pass them into this ApolloClient function (also a library class):
#discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
publishResultToStore: Bool = true,
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return self.networkTransport.send(
operation: mutation,
cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
contextIdentifier: nil,
callbackQueue: queue,
completionHandler: { result in
resultHandler?(result)
}
)
}
I can't figure out how to deal with ConcreteMutation in a generic way. I want to be able to write a factory function like so:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
The fact that this func is in an enum is irrelevant to me: that same code could be in a struct/class/whatever. What matters is the function signature. I want a factory method that returns a GraphQLMutation that can be passed into ApolloClient.perform()
Because I can't figure out a way to do either of those things, I end up writing a bunch of functions like this instead:
func useConcreteMutation1(value) -> Void {
let mutation = ConcreteMutation1(first_name: value)
apolloClient.perform(mutation: mutation)
}
func useConcreteMutation2(value) -> Void {
let mutation = ConcreteMutation2(last_name: value)
apolloClient.perform(mutation: mutation)
}
...
That's a lot of duplicated code.
Depending on how I fiddle with my getMutation signature -- e.g., <T: GraphQLMutation>() -> T? etc. -- I can get the func to compile, but I get a different compile error when I try to pass it into ApolloClient.perform(). Something saying "protocol can only be used as a generic constraint because it has Self or associated type requirements."
I've researched this a lot, and my research found this article, but I don't think it's an option if the concrete classes implementing the associated type are final?
It's really difficult to figure out if it's possible to use polymorphism in this situation. I can find plenty of articles of what you can do, but no articles on what you can't do. My question is:
How do I write getMutation so it returns a value that can be passed into ApolloClient.perform()?
The fundamental problem you are running into is that this function signature:
func getMutation<T: GraphQLMutation>() -> T
is ambiguous. The reason it's ambiguous is because GraphQLMutation has an associated type (Data) and that information doesn't appear anywhere in your function declaration.
When you do this:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
Each of those branches could have a different type. ConcreteMutation1 could have a Data that is Dormouse while ConcreteMutation3 might have a data value that's an IceCreamTruck. You may be able to tell the compiler to ignore that but then you run into problems later because Dormouse and IceCreamTruck are two structs with VERY different sizes and the compiler might need to use different strategies to pass them as parameters.
Apollo.perform is also a template. The compiler is going to write a different function based on that template for each type of mutation you call it with. In order to do that must know the full type signature of the mutation including what its Data associated type is. Should the responseHandler callback be able to handle something the size of a Dormouse, or does it need to be able to handle something the size of an IceCreamTruck?
If the compiler doesn't know, it can't set up the proper calling sequence for the responseHandler. Bad things would happen if you tried to squeeze something the size of an IceCreamTruck through a callback calling sequence that was designed for a parameter the size of a Dormouse!
If the compiler doesn't know what type of Data the mutation has to offer, it can't write a correct version of perform from the template.
If you've only handed it the result of func getMutation<T: GraphQLMutation>() -> T where you've eliminated evidence of what the Data type is, it doesn't know what version of perform it should write.
You are trying to hide the type of Data, but you also want the compiler to create a perform function where the type of Data is known. You can't do both.
Maybe you need to implement AnyGraphQLMutation type erased over the associatedtype.
There are a bunch of resources online for that matter (type erasure), I've found this one pretty exhaustive.
I hope this helps in someway:
class GraphQLQueryHelper
{
static let shared = GraphQLQueryHelper()
class func performGraphQLQuery<T:GraphQLQuery>(query: T, completion:#escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().fetch(query: query, cachePolicy: .default) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle Network error*/
break
}
}
}
class func peroformGraphQLMutation<T:GraphQLMutation>(mutation: T, completion:#escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().perform(mutation: mutation) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle error*/
break
}
}
}
}
So i have this code where i'm trying to make an task handler for requests. But in some cases the request doesn't get an model in response and therefor i don't want it to process any data. Hard to explain, but code shown below:
class UserTask<T: Codable>: ExecuteProtocol {
let userType: UserRequests
init(userType: UserRequests) {
self.userType = userType
}
var request: URLRequest {
return userType.build
}
public func run(completion: #escaping ((Response<T, NAError>) ->())) {
executeRequest(request: request) { (response) in
switch response {
case .success(let data):
completion(NADecoder<T>.decode(data: data).model)
break
case .failure(let error):
completion(.failure(error))
break
}
}
}
}
class UserTask: ExecuteProtocol {
let userType: UserRequests
init(userType: UserRequests) {
self.userType = userType
}
var request: URLRequest {
return userType.build
}
public func run(completion: #escaping ((Response<Any?, NAError>) ->())) {
executeRequest(request: request) { (response) in
switch response {
case .success(let data):
completion(.success(nil))
break
case .failure(let error):
completion(.failure(error))
break
}
}
}
}
This of course say Invalid redeclaration of 'UserTask' But can i do this in any smooth way? I have tried making the Codable optional and then unwrapping it. But as i want to keep the type of it in Decodable purpose it doesn't seem to work.
Any suggestions?
There is no need to create multiple classes for same functionality. You simply need to make some changes to a single class to support both your use-cases.
Instead of adding generic <T> to the class UserTask, add it to method run(completion:), i.e.
class UserTask: ExecuteProtocol {
let userType: UserRequests
init(userType: UserRequests) {
self.userType = userType
}
var request: URLRequest {
return userType.build
}
public func run<T: Codable>(type: T.Type, completion: #escaping ((Response<T?, NAError>) ->())) {
//your code here...
}
}
Call it like,
task.run(type: YourType.self) { (response) in
//add your code here...
}
I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033
This is my Mock for testing:
import Foundation
#testable import TraktTest
class MockUrlSessionProvider: ProviderProtocol {
enum Mode {
case success
case empty
case fail
}
private var mode: Mode
init(mode: Mode) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
switch mode {
case .success: completion(NetworkResponse.success(T))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))
If I send this to my completion success it compile:
try? JSONDecoder().decode(T.self, from: data!)
(dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.
I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:
final class URLSessionProvider: ProviderProtocol {
private var session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
let request = URLRequest(service: service)
session.dataTask(request: request) { [weak self] data, response, error in
let httpResponse = response as? HTTPURLResponse
self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
}.resume()
}
private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }
switch response.statusCode {
case 200...299:
guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
completion(.success(model))
default: completion(.failure(.unknown("no_response".localized())))
}
}
}
URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).
My Network responses are a couple of enums:
enum NetworkResponse<T> {
case success(T)
case failure(NetworkError)
}
enum NetworkError {
case unknown(String)
case noData
}
My provider protocol just have a function to make the request using generics:
protocol ProviderProtocol {
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping(NetworkResponse<T>) -> Void) where T: Decodable
}
I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:
typealias Headers = [String: String]
typealias Parameters = [String: Any]
protocol ServiceProtocol {
func baseURL() -> URL
var path: String? { get }
var id: String? { get }
var method: HTTPMethod { get }
var task: Task { get }
var headers: Headers? { get }
var parametersEncoding: ParametersEncoding { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum Task {
case requestPlain
case requestParameters(Parameters)
}
enum ParametersEncoding {
case url
case json
}
In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.
I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).
The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.
So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.
I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:
class MockUrlSessionProvider: ProviderProtocol {
// making the enum generic, to support injecting the success value
enum Mode<T> {
case success(T)
case empty
case fail
}
// need to have this as `Any` to cover all possible T generic arguments
private var mode: Any
// however the initializer can be very specific
init<T>(mode: Mode<T>) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
// if the mock was not properly configured, do nothing
guard let mode = mode as? Mode<T> else { return }
// alternatively you force cast and have the unit test crash, this should help catching early configuration issues
// let mode = mode as! Mode<T>
switch mode {
case let .success(value): completion(NetworkResponse.success(value))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I have the following Swift functions:
private func authorizedMutableURLRequest(#ref: String) -> Result<NSMutableURLRequest> {
...
}
public func fetch(#ref: String) -> Result<NSURLRequest> {
switch authorizedMutableURLRequest(ref: ref) {
case .Success(let mutableURLRequestBox):
return .Success(JiveBox(mutableURLRequestBox.value))
case .Failure(let error):
return .Failure(error)
}
}
There must be a better way to write fetch. If this was Java, I'd write:
public Result<? extends NSURLRequest> fetch(String ref) {
return authorizedMutableURLRequest(ref);
}
private Result<NSMutableURLRequest> authorizedMutableURLRequest(String ref) {
...
}
And everything would work correctly. Is there a way to return an unspecified type with an upper bound in Swift like there is in Java?