Skip to content

Instantly share code, notes, and snippets.

@rdv0011
Last active December 9, 2019 14:00
Show Gist options
  • Save rdv0011/83628dfd08e51d41d2ab97dcd402a950 to your computer and use it in GitHub Desktop.
Save rdv0011/83628dfd08e51d41d2ab97dcd402a950 to your computer and use it in GitHub Desktop.
A robust way to encode HTTP GET parameters

The code below demonstrates how to extend Encodable protocol to get parameters for the HTTP GET request directly out of the swift structure. Using this way there is no need to manually create an error prone dictionary of parameters. This code also shows one of the ways how to handle HTTP errors when making data task requests using URLSession dataTask publisher.

import UIKit
import Combine
import PlaygroundSupport

extension Encodable {
    var dictionary: [String: Any]? {
        guard let data = try? JSONEncoder().encode(self) else {
            return nil
        }
        return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
    }

    var queryItems: [URLQueryItem]? {
        dictionary?.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) }
    }
}

struct WeatherQueryParams: Encodable {
    let lat: Int
    let lon: Int
    let appid: String
}

struct WeatherParameters: Decodable {
    let temp: Double
}

struct WeatherResponse: Decodable {
    let main: WeatherParameters
    static var placeholder: WeatherResponse {
        return WeatherResponse(main: WeatherParameters(temp: 0.0))
    }
}
enum WeatherAPIError: Error {
    case invalidRequest
    case invalidResponse(String)
    case noError

    init(data: Data?, response: URLResponse) {
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            // TODO: implement parsing of the error from JSON response
            self = WeatherAPIError.invalidResponse("\(String(describing: data))")
            return
        }
        self = .noError
    }

    init(urlError: URLError) {
        // TODO: implement mapping URLError to WeatherAPIError values
        self = WeatherAPIError.invalidResponse("\(urlError.localizedDescription)")
    }

    init(jsonDecodingError: Error) {
        self = .invalidResponse(jsonDecodingError.localizedDescription)
    }
}

func createRequest() -> Result<URLRequest, Error> {
    let baseURL = "https://samples.openweathermap.org"
    guard var urlComponents = URLComponents(string: baseURL) else {
        return .failure(WeatherAPIError.invalidRequest)
    }
    urlComponents.path = "/data/2.5/weather"
    // Please note how a structure is converted into query parameters. This way is safer than using a Dictionary.
    urlComponents.queryItems = WeatherQueryParams(lat: 35, lon: 139, appid: "b6907d289e10d714a6e88b30761fae22").queryItems
    guard let url = urlComponents.url else {
        return .failure(WeatherAPIError.invalidRequest)
    }
    return .success(URLRequest(url: url))
}

func sendRequest(urlRequest: URLRequest) -> AnyPublisher<WeatherResponse, Error> {
    URLSession.shared.dataTaskPublisher(for: urlRequest)
        .catch{ err in
            // URLError handling
            return Fail<(data: Data, response: URLResponse), Error>(error: WeatherAPIError(urlError: err)).eraseToAnyPublisher()
        }
        .tryMap { data, response -> Data in
            // HTTP error codes handling if any
            let apiError = WeatherAPIError(data: data, response: response)
            guard case .noError = apiError else {
                throw apiError
            }
            return data
        }
        .decode(type: WeatherResponse.self, decoder: JSONDecoder())
        .catch { (jsonDecodingError) -> AnyPublisher<WeatherResponse, Error> in
            // JSON decoding errors handling
            return Fail<WeatherResponse, Error>(error: WeatherAPIError(jsonDecodingError: jsonDecodingError)).eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
}

func checkWeather() -> AnyPublisher<WeatherResponse, Error> {
    let result = createRequest()
    switch result {
    case .failure(let error):
        return Fail<WeatherResponse, Error>(error: error).eraseToAnyPublisher()
    case .success(let urlRequest):
        return sendRequest(urlRequest: urlRequest).eraseToAnyPublisher()
    }
}

var cancellableSet: Set<AnyCancellable> = []
checkWeather().sink(receiveCompletion: { (completion) in
    print(completion)
}) { (weather) in
    print(weather)
}
.store(in: &cancellableSet)
PlaygroundPage.current.needsIndefiniteExecution = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment