Created
April 12, 2016 14:22
-
-
Save delebedev/2c44a556da2df05d17842ef1465bed72 to your computer and use it in GitHub Desktop.
Tiny networking revisited
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 Result | |
import Unbox | |
public enum Method: String { // Bluntly stolen from Alamofire | |
case OPTIONS = "OPTIONS" | |
case GET = "GET" | |
case HEAD = "HEAD" | |
case POST = "POST" | |
case PUT = "PUT" | |
case PATCH = "PATCH" | |
case DELETE = "DELETE" | |
case TRACE = "TRACE" | |
case CONNECT = "CONNECT" | |
} | |
public struct Resource<A> { | |
let path: String | |
let method : Method | |
let parameters: [String: AnyObject]? | |
let headers : [String: String] | |
let transform: NSData -> Result<A, NSError> | |
} | |
public class Error: Unboxable { | |
let code: String | |
let message: String | |
required public init(unboxer: Unboxer) { | |
self.code = unboxer.unbox("error.code", isKeyPath: true) | |
self.message = unboxer.unbox("error.message", isKeyPath: true) | |
} | |
} | |
public enum HTTPFailureReason { | |
case NoData | |
case NoSuccessStatusCode(statusCode: Int, error: Error?) | |
case Other(NSError) | |
} | |
extension HTTPFailureReason: ErrorType { } | |
public protocol Cancellable { | |
func cancel() | |
} | |
extension NSURLSessionTask: Cancellable {} | |
public func apiRequest<A>(baseURL: NSURL, resource: Resource<A>, completion: Result<A, HTTPFailureReason> -> ()) -> Cancellable { | |
let session = NSURLSession.sharedSession() | |
let url = baseURL.URLByAppendingPathComponent(resource.path) | |
let request = NSMutableURLRequest(URL: url) | |
request.HTTPMethod = resource.method.rawValue | |
//do not attach nil parameters | |
//do not createrequest prior to getting param type | |
if resource.method == .GET { | |
let comps = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)! | |
comps.queryItems = (resource.parameters ?? [:]).map { NSURLQueryItem(name: $0.0, value: "\($0.1)") } | |
request.URL = comps.URL | |
} else { | |
request.HTTPBody = encodeJSON(resource.parameters ?? [:]) | |
} | |
for (key,value) in resource.headers { | |
request.setValue(value, forHTTPHeaderField: key) | |
} | |
let task = session.dataTaskWithRequest(request) { (data, response, error) in | |
guard let httpResponse = response as? NSHTTPURLResponse else { | |
return completion(Result(error: .Other(error!))) // remove ! | |
} | |
guard let responseData = data else { | |
return completion(Result(error: .NoData)) | |
} | |
guard 200..<300 ~= httpResponse.statusCode else { | |
return completion(Result(error:.NoSuccessStatusCode(statusCode: httpResponse.statusCode, error: Unbox(responseData)))) | |
} | |
completion(resource.transform(responseData).mapError { .Other($0) }) | |
} | |
task.resume() | |
return task | |
} | |
func encodeJSON(dict: JSONDictionary) -> NSData? { | |
//TODO: do not swallow error | |
return dict.count > 0 ? try? NSJSONSerialization.dataWithJSONObject(dict, options: NSJSONWritingOptions()) : nil | |
} | |
public typealias JSONDictionary = [String:AnyObject] | |
public func jsonResource<A where A: Unboxable>(path: String, method: Method, parameters: [String: AnyObject]? = nil, headers: [String: String]? = nil) -> Resource<A> { | |
var allHeaders = ["Content-Type": "application/json"] | |
// ??[:] is ugly | |
for (key, value) in headers ?? [:] { | |
allHeaders[key] = value | |
} | |
let transform: NSData -> Result<A, NSError> = { data in materialize { try UnboxOrThrow(data) } } | |
return Resource(path: path, method: method, parameters: parameters, headers: allHeaders, transform: transform) | |
} | |
// Specific | |
public func getProductCategories() -> Resource<Collection<ShopCategory>> { | |
return jsonResource("v1/categories", method: .GET, parameters: ["per_page" : 100]) | |
} | |
public func getProduct(ID: Int) -> Resource<Product> { | |
return jsonResource("v1/products/\(ID)", method: .GET) | |
} | |
public func login(login: String, password: String) -> Resource<User> { | |
let params = ["login": login, "password": password] | |
return jsonResource("v1/users/login", method: .POST, parameters: params) | |
} | |
public func getCards() -> Resource<Collection<Card>> { | |
return secureJsonResource("v1/users/cards", method: .GET, parameters: ["per_page" : 100]) | |
} | |
public func createCard(token: String) -> Resource<Card> { | |
return secureJsonResource("v1/users/cards", method: .POST, parameters: ["token": token]) | |
} | |
public func removeCard(ID: Int) -> Resource<OperationStatus> { | |
return secureJsonResource("v1/users/cards/\(ID)", method: .DELETE) | |
} | |
func secureJsonResource<A where A: Unboxable>(path: String, method: Method, parameters: [String: AnyObject]? = nil) -> Resource<A> { | |
guard let token = SwiftGift.token else { preconditionFailure("Token expected to be set before secure request.") } | |
return jsonResource(path, method: method, parameters: parameters, headers: ["X-Access-Token": token]) | |
} | |
public enum SwiftGiftError { | |
case HTTP(NSError) | |
case API(Error) | |
case Other | |
} | |
extension SwiftGiftError: ErrorType {} | |
public struct SwiftGift { | |
static var token: String? = nil | |
static public func request<A>(resource: Resource<A>, completion: Result<A, SwiftGiftError> -> ()) -> Cancellable { | |
return apiRequest(NSURL(string: "https://example.com")!, resource: resource) { result in | |
//swallow cancelled requests | |
if case let .Other(e)? = result.error where e.isCancelled { | |
return | |
} | |
completion(result.mapError(promoteHTTPErrorToAppError)) | |
} | |
} | |
private static func promoteHTTPErrorToAppError(error: HTTPFailureReason) -> SwiftGiftError { | |
switch error { | |
case let .NoSuccessStatusCode(_, error: apiError?): | |
return .API(apiError) | |
case let .Other(error): | |
return .HTTP(error) | |
default: | |
return .Other | |
} | |
} | |
} | |
private extension NSError { | |
var isCancelled: Bool { | |
return domain == NSURLErrorDomain && code == NSURLErrorCancelled | |
} | |
} | |
//model | |
public struct OperationStatus: Unboxable { | |
public init(unboxer: Unboxer) { | |
} | |
} | |
public class ShopCategory: Unboxable { | |
public let ID: Int | |
public let imageUrl: String? | |
required public init(unboxer: Unboxer) { | |
self.ID = unboxer.unbox("id") | |
self.imageUrl = unboxer.unbox("image_url") | |
} | |
} | |
public struct Collection<A where A: Unboxable>: Unboxable { | |
let items: [A] | |
public init(unboxer: Unboxer) { | |
items = unboxer.unbox("collection") | |
} | |
} | |
public class Product: NSObject, Unboxable { | |
let name: String | |
public required init(unboxer: Unboxer) { | |
name = unboxer.unbox("name") | |
} | |
} | |
public class User: NSObject, Unboxable { | |
let email: String | |
let token: Token | |
public required init(unboxer: Unboxer) { | |
email = unboxer.unbox("email") | |
token = unboxer.unbox("token") | |
} | |
} | |
public struct Token: Unboxable { | |
let token: String | |
let refreshToken: String | |
let expires: NSTimeInterval | |
public init(unboxer: Unboxer) { | |
token = unboxer.unbox("value") | |
refreshToken = unboxer.unbox("refresh_token") | |
expires = unboxer.unbox("expires") | |
} | |
} | |
public struct Card: Unboxable { | |
let ID: Int | |
let stripeID: String | |
let last4: String | |
let vendor: String | |
let expirationMonth: Int | |
let expirationYear: Int | |
public init(unboxer: Unboxer) { | |
ID = unboxer.unbox("id") | |
stripeID = unboxer.unbox("card_id") | |
last4 = unboxer.unbox("last4") | |
vendor = unboxer.unbox("type") | |
expirationMonth = unboxer.unbox("expiration_month") | |
expirationYear = unboxer.unbox("expiration_year") | |
} | |
} |
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
SwiftGift.request(getProductCategories()) { result in | |
print(result) | |
} | |
let token = SwiftGift.request(login("[email protected]", password: "notabc23")) { result in | |
} | |
token.cancel() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment