Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Playground with example code for Wirekit
import Foundation
import Combine
// The Request Method
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
enum NetworkRequestError: LocalizedError, Equatable {
case invalidRequest
case badRequest
case unauthorized
case forbidden
case notFound
case error4xx(_ code: Int)
case serverError
case error5xx(_ code: Int)
case decodingError
case urlSessionFailed(_ error: URLError)
case unknownError
}
// Extending Encodable to Serialize a Type into a Dictionary
extension Encodable {
var asDictionary: [String: Any] {
guard let data = try? JSONEncoder().encode(self) else { return [:] }
guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
return [:]
}
return dictionary
}
}
// Our Request Protocol
protocol Request {
var path: String { get }
var method: HTTPMethod { get }
var contentType: String { get }
var body: [String: Any]? { get }
var headers: [String: String]? { get }
associatedtype ReturnType: Codable
}
// Defaults and Helper Methods
extension Request {
// Defaults
var method: HTTPMethod { return .get }
var contentType: String { return "application/json" }
var queryParams: [String: String]? { return nil }
var body: [String: Any]? { return nil }
var headers: [String: String]? { return nil }
/// Serializes an HTTP dictionary to a JSON Data Object
/// - Parameter params: HTTP Parameters dictionary
/// - Returns: Encoded JSON
private func requestBodyFrom(params: [String: Any]?) -> Data? {
guard let params = params else { return nil }
guard let httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) else {
return nil
}
return httpBody
}
/// Transforms an Request into a standard URL request
/// - Parameter baseURL: API Base URL to be used
/// - Returns: A ready to use URLRequest
func asURLRequest(baseURL: String) -> URLRequest? {
guard var urlComponents = URLComponents(string: baseURL) else { return nil }
urlComponents.path = "\(urlComponents.path)\(path)"
guard let finalURL = urlComponents.url else { return nil }
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
request.allHTTPHeaderFields = headers
return request
}
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
/// Parses a HTTP StatusCode and returns a proper error
/// - Parameter statusCode: HTTP status code
/// - Returns: Mapped Error
private func httpError(_ statusCode: Int) -> NetworkRequestError {
switch statusCode {
case 400: return .badRequest
case 401: return .unauthorized
case 403: return .forbidden
case 404: return .notFound
case 402, 405...499: return .error4xx(statusCode)
case 500: return .serverError
case 501...599: return .error5xx(statusCode)
default: return .unknownError
}
}
/// Parses URLSession Publisher errors and return proper ones
/// - Parameter error: URLSession publisher error
/// - Returns: Readable NetworkRequestError
private func handleError(_ error: Error) -> NetworkRequestError {
switch error {
case is Swift.DecodingError:
return .decodingError
case let urlError as URLError:
return .urlSessionFailed(urlError)
case let error as NetworkRequestError:
return error
default:
return .unknownError
}
}
}
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
guard let urlRequest = request.asURLRequest(baseURL: baseURL) else {
return Fail(outputType: R.ReturnType.self, failure: NetworkRequestError.badRequest).eraseToAnyPublisher()
}
typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
let requestPublisher: RequestPublisher = networkDispatcher.dispatch(request: urlRequest)
return requestPublisher.eraseToAnyPublisher()
}
}
// Performing some requests
// Our Model
struct Todo: Codable {
var title: String
var completed: Bool
}
// Request
struct FindTodos: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
}
// POST Request
struct AddTodo: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
var method: HTTPMethod = .post
var body: [String: Any]
init(body: [String: Any]) {
self.body = body
}
}
private var cancellables = [AnyCancellable]()
let dispatcher = NetworkDispatcher()
let apiClient = APIClient(baseURL: "https://jsonplaceholder.typicode.com")
// Simple GET Request
apiClient.dispatch(FindTodos())
.sink(receiveCompletion: { _ in },
receiveValue: { value in
print(value)
})
.store(in: &cancellables)
// A Simple Post Request
let otherTodo: Todo = Todo(title: "Test", completed: true)
apiClient.dispatch(AddTodo(body: otherTodo.asDictionary))
.sink(receiveCompletion: { result in
// Do something after adding...
},
receiveValue: { _ in })
.store(in: &cancellables)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment