Created
April 19, 2019 21:09
-
-
Save rnapier/54ea4a0faa8f987d9ca452b354366296 to your computer and use it in GitHub Desktop.
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 | |
// Provides a nice bare-init for ID types | |
protocol IDType: Codable, Hashable { | |
associatedtype Value | |
var value: Value { get } | |
init(value: Value) | |
} | |
extension IDType { | |
init(_ value: Value) { self.init(value: value) } | |
} | |
// A couple of concrete model types | |
struct User: Codable, Equatable { | |
struct ID: IDType { let value: Int } | |
let id: ID | |
let name: String | |
} | |
struct Document: Codable, Equatable { | |
struct ID: IDType { let value: String } | |
let id: ID | |
let title: String | |
} | |
// The base URL of our API | |
let baseURL = URL(string: "https://www.example.org")! | |
// In order to be fetched using `fetch`, the type must implement Fetchable | |
protocol Fetchable: Decodable { | |
static var apiBase: String { get } | |
associatedtype ID: IDType | |
var id: ID { get } | |
} | |
// Types like User and Document implement fetchable. | |
extension User: Fetchable { | |
static var apiBase: String { return "user" } | |
} | |
extension Document: Fetchable { | |
static var apiBase: String { return "document" } | |
} | |
// A low-level transport that asynchronously converts URLRequests into Data. | |
// This can be anything that achieves (URLRequest) -> Data. URLSession, a database, | |
// a cache, pre-loaded data for unit testing, whatever. | |
protocol Transport { | |
func fetch(request: URLRequest, | |
completion: @escaping (Result<Data, Error>) -> Void) | |
} | |
// URLSession can be a Transport. We *should* be able to directly conform URLSession | |
// to Transport, but we can't beause of a current Swift compiler bug. | |
// https://bugs.swift.org/browse/SR-10481 | |
class NetworkTransport: Transport { | |
static let shared = NetworkTransport() | |
let session: URLSession | |
init(session: URLSession = .shared) { self.session = session } | |
func fetch(request: URLRequest, | |
completion: @escaping (Result<Data, Error>) -> Void) | |
{ | |
session.dataTask(with: request) { (data, _, error) in | |
if let error = error { completion(.failure(error)) } | |
else if let data = data { completion(.success(data)) } | |
}.resume() | |
} | |
} | |
// When SR-10481 is fixed, replace NetworkTransport with: | |
// typealias NetworkTransport = URLSession | |
//extension URLSession: Transport { | |
// func fetch(request: URLRequest, | |
// completion: @escaping (Result<Data, Error>) -> Void) | |
// { | |
// self.dataTask(with: request) { (data, _, error) in | |
// if let error = error { completion(.failure(error)) } | |
// else if let data = data { completion(.success(data)) } | |
// }.resume() | |
// } | |
//} | |
// A wrapper to hold a transport | |
struct Client { | |
let transport: Transport | |
init(transport: Transport = NetworkTransport.shared) { | |
self.transport = transport | |
} | |
} | |
// A Request for the Client | |
struct Request { | |
let urlRequest: URLRequest | |
let completion: (Result<Data, Error>) -> Void | |
} | |
// Different API end points construct Request differently | |
extension Request { | |
// GET /<model>/<id> -> Model | |
static func fetching<Model: Fetchable>( | |
_: Model.Type, | |
id: Model.ID, | |
completion: @escaping (Result<Model, Error>) -> Void) | |
-> Request | |
{ | |
let urlRequest = URLRequest(url: baseURL | |
.appendingPathComponent(Model.apiBase) | |
.appendingPathComponent("\(id)") | |
) | |
return self.init(urlRequest: urlRequest) { | |
data in | |
completion(Result { | |
let decoder = JSONDecoder() | |
return try decoder.decode( | |
Model.self, | |
from: data.get()) | |
}) | |
} | |
} | |
} | |
extension Request { | |
// POST /keepalive -> Error? | |
static func keepAlive( | |
completion: @escaping (Error?) -> Void) | |
-> Request | |
{ | |
var urlRequest = URLRequest(url: baseURL | |
.appendingPathComponent("keepalive") | |
) | |
urlRequest.httpMethod = "POST" | |
return self.init(urlRequest: urlRequest) { | |
switch $0 { | |
case .success: completion(nil) | |
case .failure(let error): | |
completion(error) | |
} | |
} | |
} | |
} | |
// And we can use Requests with Clients | |
extension Client { | |
func fetch(request: Request) { | |
transport.fetch(request: request.urlRequest, | |
completion: request.completion) | |
} | |
} | |
// Transports are very powerful. They can be chained. This one takes a transport | |
// and adds custom headers to it. | |
struct AddHeaders: Transport | |
{ | |
func fetch(request: URLRequest, | |
completion: @escaping (Result<Data, Error>) -> Void) | |
{ | |
var newRequest = request | |
for (key, value) in headers { | |
newRequest.addValue(value, forHTTPHeaderField: key) | |
} | |
base.fetch(request: newRequest, completion: completion) | |
} | |
let base: Transport | |
var headers: [String: String] | |
} | |
let transport = AddHeaders(base: NetworkTransport.shared, | |
headers: ["Authorization": "..."]) | |
let client = Client(transport: transport) | |
let request = Request.fetching(User.self, | |
id: User.ID(0), | |
completion: { print($0) }) | |
client.fetch(request: request) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment