Skip to content

Instantly share code, notes, and snippets.

@rnapier
Created April 19, 2019 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rnapier/54ea4a0faa8f987d9ca452b354366296 to your computer and use it in GitHub Desktop.
Save rnapier/54ea4a0faa8f987d9ca452b354366296 to your computer and use it in GitHub Desktop.
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