I was reading this article http://chris.eidhof.nl/post/reducers/ when I found a puzzling piece of swift I realized I couldn't quite understand.
Given these enums
enum Message {
case inputChanged(String?)
case ratesAvailable(data: Data?)
case reload
}
enum Command {
case load(URL, onComplete: (Data?) -> Message)
}
in the following function
mutating func send(_ message: Message) -> Command? {
switch message {
case .inputChanged(let input):
inputAmount = input.flatMap { Double($0) }
return nil
case .ratesAvailable(data: let data):
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []),
let dict = json as? [String:Any],
let dataDict = dict["rates"] as? [String:Double],
let rate = dataDict[Currency.usd.rawValue] else { return nil }
self.rate = rate
return nil
case .reload:
return .load(ratesURL(), onComplete: Message.ratesAvailable)
}
}
In the last line of this function. How can the enum Message.ratesAvailable can be assigned as the clousure parameter defined in the Command enum associated value?
Because Message.ratesAvailable has type (Data?) -> Message
which you can see for yourself:
import Foundation
enum Message {
case inputChanged(String?)
case ratesAvailable(data: Data?)
case reload
}
print(type(of: Message.ratesAvailable)) // => (Optional<Data>) -> Message
Related
I am trying to run test case for Failure response . I have an empty json file into project and named it FailureResponse . This file is empty . I trying to count the number of array is empty for example ..
XCTAssertTrue(schools.count==0)
It should pass the test because the json file is empty .
same result fields like school name and School location etc but the problem is it showing error ..
testFailure(): Asynchronous wait failed: Exceeded timeout of 6 seconds, with unfulfilled expectations: "waiting for response".
View Model code...
import Foundation
import Combine
class ViewModel {
private let networkManager = NetworkManager()
#Published private(set) var school = [School]()
func getSchools() {
loadMoreSchools()
}
func loadMoreSchools() {
let newURL = NetworkURLs.baseURL
networkManager
.getModel([School].self, from: newURL) { [weak self] result in
switch result {
case .success(let schoolResponse):
self?.school = schoolResponse
print(schoolResponse)
case .failure(let error):
print(error)
}
}
}
func getSchoolName(by row: Int) -> String {
let schoolName = school[row]
return schoolName.schoolName.uppercased()
}
func getSchoolLocation(by row: Int) -> String {
return "\(school[row].location)"
}
}
Here is my Mock service call ..
class MockService: NetworkManagerProtocol {
var data: Data?
func getModel<Model>(_ type: Model.Type, from url: String, completion: #escaping (Result<Model, Alomafire_Project.NetworkError>) -> ()) where Model : Decodable, Model : Encodable {
if let data = data {
do {
let result = try JSONDecoder().decode(type, from: data)
completion(.success(result))
} catch (let error){
print(error)
}
}
}
}
Here is code for call the local Jason ..
func getData(json: String) throws -> Data {
guard let url = Bundle(for: Alomafire_ProjectTests.self).url(forResource: json, withExtension: "json")
else { return Data() }
return try Data(contentsOf: url)
}
Here is the test case ....
func testFailure() throws {
// Given
mockService.data = try getData(json: "FailureResponse")
var schools: [School] = []
let expectation = expectation(description: "waiting for response")
// When
viewModel?
.$school
.dropFirst()
.sink(receiveValue: { result in
schools = result
expectation.fulfill()
})
.store(in: &subscribers)
// viewModel?.getSchools()
// Then
waitForExpectations(timeout: 10.0)
XCTAssertTrue(schools.count==0)
}
Here is the debug result . it return 0 ..
Here is the screenshot of the result ..
You mention in the question that "the json file is empty." If that is the case, then this test will fail. The MockService assumes that the Data pulled from the json file will be decodable to the type requested. If it isn't the getModel(_:from:completion:) will never call the completion and the test will not complete in the specified time limit. Solve this by calling the completion closure even when the JSONDecoder response with an error.
Also, even if that mock emits the error properly, your ViewModel doesn't do anything with it that would cause the schools type to update.
I have RestManager class which is used for fetching data from Internet and is returning AnyPublisher
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap({ data, _ in
let value = try JSONDecoder().decode(T.self, from: data)
if let array = value as? Array<Any>, array.isEmpty {
throw ErrorType.empty
}
return value
})
.mapError { error -> ErrorType in
switch error {
case is ErrorType:
return ErrorType.empty
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}
}
Repository has two functions (getWorldwideData and getCountryData returning AnyPublisher<(WorldwideResponse item or CountryResponse item), ErrorType>)
In viewModel, I made these functions.
private func getData() {
$useCaseSelection
.flatMap { value -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> in
self.loader = true
self.error = nil
switch value {
case let .country(name):
return self.countryPipeline(name: name)
case .worldwide:
return self.worldwidePipeline()
}
}
.mapError { error in
self.error = error
}
.assign(to: &$homeScreenDomainItem)
}
private func worldwidePipeline() -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getWorldwideData()
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(worldwideResponseItem: response)
}
.eraseToAnyPublisher()
}
private func countryPipeline(name: String) -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getCountryData(for: name)
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(countryDayOneStatsResponse: response)
}
.eraseToAnyPublisher()
}
I wanted to make clean code, so I split code into two separate function based on useCaseSelection.
useCaseSelection is enum with two types.
error is ErrorType? value wrapped with #Published, in which I want to save error type if there is any error.
homeScreenDomainItem is Covid19StatisticsDomainItem instance wrapped with #Published.
Problem is in getData function where in MapError pipeline I am getting:
Cannot convert value of type () to closure result type Never
I tried to use setFailureType(to: ErrorType.self) but that is not helping.
I followed the directions on the firebase website and came up with the code below labeled NEW CODE. The error I am getting is:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
I have tried researching about the Result object and this error but have not found anything that would help me directly. I was able to read the data using the old way (which I will post below), but am trying to follow Googles documentation
(https://firebase.google.com/docs/firestore/query-data/get-data)
Any help would be appreciated, thank you!
OLD CODE
func readData(word: String) -> CombinedModel? {
print("reading data")
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
var wordResults: CombinedModel? = nil
docRef.getDocument { (document, error) in
if let e = error {
print("Error loading data: \(e)")
return
} else {
do {
if let resultData = try document?.data(as: CombinedModel.self){
print("Definitions: \(resultData.definitionsArray)")
print("\n Synonyms: \(resultData.synonyms)")
wordResults = resultData
}
} catch {
print("Error decoding: \(error)")
}
}
}
return wordResults
}
NEW CODE
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
print("CombinedModel: \(combinedModel)")
} else {
print("Document does not exist")
}
case .failure(let error):
print("Error decoding city: \(error)")
}
}
}
Here is the codable custom class I created for reading the data.
struct CombinedModel: Codable {
var definitionsArray: [WordModel]
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case definitionsArray
case synonyms
}
}
struct WordModel: Codable {
let id: String
let partOfSpeech: String
let definitions: [String]
let example: [String]
let ipa: String
let audio: String
private enum CodingKeys: String, CodingKey {
case id
case partOfSpeech
case definitions
case example
case ipa
case audio
}
}
struct ThesaurusModel: Codable {
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case synonyms
}
}
I realized the issue with the Result object was that Xcode was reading it as a structure that I created in a different file, but once I changed the name of that structure(it was named Result previously), it recognized the Result object as the correct object which is of type: enum Result where Failure : Error
Looks like you're getting close - minus the type error. From what I can tell, your error:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
Refers to this portion of code:
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
At least, this is the portion of code that looks off to me. Your (document, error) variable as a whole represents your result. You shouldn't need a Result middleman.
The real focus should be around determining whether or not there is an error. If there isn't you should read the document.
A working approach should look something like this:
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
guard let fbDoc = document else {
// document is nil, parse the error object and handle it.
}
if let resultData = try fbDoc?.data(as: CombinedModel.self) {
return resultData
} else {
return nil
}
}
}
Let me know how that goes for you! Best of luck.
I'd like to convert a value I get from an API to a specific format.
[String:Any] // format received
[Int:[ContentType:Int]] // required format
ContentType is an Enum
An example of the data might look like this:
["123":["Tables":"25","Chairs":"14"]] // input
[123:[.Tables:25,.Chairs:14]] // output
I think I need to have a map within a map for this to work, but I'm struggling to work out a way forward. I may well be barking up the wrong tree entirely though. I don't really want to manually loop through and add each item one at a time; I'm looking for something more intelligent than that if possible.
enum ContentType: String {
case Tables,Chairs
}
let original_values: [String:Any]
= ["1234":["Tables":"5","Chairs":"2"]]
let values: [Int:[ContentType:Int]]
= Dictionary(uniqueKeysWithValues: original_values.map {
(
Int($0.key)!,
(($0.value as? [String:String]).map { // Error on this line - expects 1 argument but two were used
(
ContentType(rawValue: $1.key)!, // $1 is presumably wrong here?
Int($1.value)
)
}) as? [ContentType:Int]
)
})
Any ideas anybody?
I'd like to convert a value I get from an API to a specific format.
You can make your enum Decodable
enum ContentType: String, Decodable {
case tables, chairs
enum CodingKeys: String, CodingKey {
case Tables = "Tables"
case Chairs = "Chairs"
}
}
Then you can decode received Data and then compactMap it to format (Int, [ContentType: Int]). These tuples you can convert to Dictionary using designed initializer
do {
let decoded = try JSONDecoder().decode([String: [ContentType: Int]].self, from: data)
let mapped = Dictionary(uniqueKeysWithValues: decoded.compactMap { (key,value) -> (Int, [ContentType: Int])? in
if let int = Int(key) {
return (int, value)
} else {
return nil
}
})
} catch {
print(error)
}
On this line:
(($0.value as? [String:String]).map {
You using not Sequence.map, but Optional.map.
Working solution:
/// First let's map plain types to our types
let resultArray = original_values
.compactMap { (key, value) -> (Int, [ContentType: Int])? in
guard let iKey = Int(key), let dValue = value as? [String: String] else { return nil }
let contentValue = dValue.compactMap { (key, value) -> (ContentType, Int)? in
guard let cKey = ContentType(rawValue: key), let iValue = Int(value) else { return nil }
return (cKey, iValue)
}
let contentDict = Dictionary(uniqueKeysWithValues: contentValue)
return (iKey, contentDict)
}
let result = Dictionary(uniqueKeysWithValues: resultArray)
To improve print output add conform to CustomStringConvertible:
extension ContentType: CustomStringConvertible {
var description: String {
switch self {
case .Tables:
return "Tables"
case .Chairs:
return "Chairs"
}
}
}
This is Swift 5 correct syntax
enum ContentType: String {
case tables = "Tables"
case chairs = "Chairs"
}
let originalValues: [String: [String: String]]
= ["1234": ["Tables": "5", "Chairs": "2"]]
let values: [Int: [ContentType: Int]] = Dictionary(uniqueKeysWithValues:
originalValues.map { arg in
let (key, innerDict) = arg
let outMap: [ContentType: Int] = Dictionary(uniqueKeysWithValues:
innerDict.map { innerArg in
let (innerKey, innerValue) = innerArg
return (ContentType.init(rawValue: innerKey)!, Int(innerValue)!)
}
)
return (Int(key)!, outMap)
}
)
print(values)
[1234: [__lldb_expr_5.ContentType.tables: 5, __lldb_expr_5.ContentType.chairs: 2]]
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")))
}
}
}