Last active
June 14, 2017 11:38
-
-
Save sundeepgupta/442601b116098059a178eed4dc0255d3 to your computer and use it in GitHub Desktop.
Code for Roll your own networking in Swift blog article: http://sundeepgupta.ca/post/161796783473/roll-your-own-swift-networking-code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: - Public | |
public struct WebService { | |
let actionManager: ActionManager | |
// As public API, optimize for simplicity by only providing a Result and not throwing. | |
func getProfile(id: String, completion: @escaping (Result<Profile>) -> Void) { | |
let action = Action<NSNull, Profile>(urlString: "api.com/profile", method: "GET", body: NSNull()) | |
perform(action, completion: completion) | |
} | |
func createProfile(_ profile: Profile, completion: @escaping (Result<NSNull>) -> Void) { | |
let action = Action<Profile, NSNull>(urlString: "api.com/profile", method: "POST", body: profile) | |
perform(action, completion: completion) | |
} | |
private func perform<Req, Res>(_ action: Action<Req, Res>, completion: @escaping (Result<Res>) -> Void) { | |
do { try actionManager.perform(action) { completion($0) } } | |
catch { completion(Result.failure(error)) } | |
} | |
} | |
public protocol Encodable { | |
func encode() throws -> Data | |
} | |
public protocol Decodable { | |
static func decode(data: Data) throws -> Self | |
} | |
public struct Profile: Encodable, Decodable { | |
let name: String | |
let age: Int | |
public static func decode(data: Data) throws -> Profile { | |
guard | |
let object = try JSONSerialization.jsonObject(with: data) as? [String: Any], | |
let name = object["name"] as? String, | |
let age = object["age"] as? Int | |
else { throw WebServiceError.invalidJSON(data) } | |
return self.init(name: name, age: age) | |
} | |
public func encode() throws -> Data { | |
let object: [String: Any] = ["name": name, "age": age] | |
return try JSONSerialization.data(withJSONObject: object) | |
} | |
} | |
extension NSNull: Encodable, Decodable { | |
public static func decode(data: Data) throws -> Self { | |
return self.init() | |
} | |
public func encode() throws -> Data { | |
return Data() | |
} | |
} | |
public enum Result<Value> { | |
case success(Value) | |
case failure(Error) | |
} | |
public enum WebServiceError: Error { | |
case invalidURL(String) | |
case invalidStatusCode(Int) | |
case invalidJSON(Data) | |
} | |
// MARK: - Internal | |
internal struct Action<RequestBody: Encodable, ResponseBody: Decodable> { | |
let urlString: String | |
let method: String | |
let body: RequestBody | |
let decode = ResponseBody.decode | |
// Anything else you may need like tokens, headers, etc. | |
func request() throws -> URLRequest { | |
guard let url = URL(string: urlString) else { throw WebServiceError.invalidURL(urlString) } | |
var request = URLRequest(url: url) | |
request.httpMethod = method | |
request.httpBody = try body.encode() | |
return request | |
} | |
} | |
internal struct ActionManager { | |
let httpManager: HTTPManager | |
func perform<Req, Res>(_ action: Action<Req, Res>, completion: @escaping (Result<Res>) -> Void) throws { | |
let request = try action.request() | |
httpManager.perform(request) { result in | |
switch result { | |
case .success(let data): | |
do { completion(Result.success(try action.decode(data))) } | |
catch { completion(Result.failure(error)) } | |
case .failure(let error): completion(Result.failure(error)) | |
} | |
} | |
} | |
} | |
internal struct HTTPManager { | |
let urlSession: URLSession | |
func perform(_ request: URLRequest, completion: @escaping (Result<Data>) -> Void) { | |
urlSession.dataTask(with: request) { data, response, error in | |
if error != nil { return completion(Result.failure(error!)) } | |
let response = response as! HTTPURLResponse // Trusting the docs for this example! | |
// Could pass in an object that deals with status codes, which would be part of the Action. | |
guard response.statusCode == 200 else { | |
return completion(Result.failure(WebServiceError.invalidStatusCode(response.statusCode))) | |
} | |
completion(Result.success(data ?? Data())) | |
}.resume() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment