Skip to content

Instantly share code, notes, and snippets.

@delebedev
Created April 12, 2016 14:22
Show Gist options
  • Save delebedev/2c44a556da2df05d17842ef1465bed72 to your computer and use it in GitHub Desktop.
Save delebedev/2c44a556da2df05d17842ef1465bed72 to your computer and use it in GitHub Desktop.
Tiny networking revisited
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")
}
}
SwiftGift.request(getProductCategories()) { result in
print(result)
}
let token = SwiftGift.request(login("d2.lebedev@gmail.com", password: "notabc23")) { result in
}
token.cancel()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment