Skip to content

Instantly share code, notes, and snippets.

@shawnthroop
Created December 7, 2019 13:21
Show Gist options
  • Save shawnthroop/f32793406d00034959b74666e5c9fbbb to your computer and use it in GitHub Desktop.
Save shawnthroop/f32793406d00034959b74666e5c9fbbb to your computer and use it in GitHub Desktop.
A simple type safe wrapper around URLSession and URLRequest.
import Foundation
public enum Method : String, Hashable {
case options, get, head, post, put, delete, trace, connect
}
public struct HeaderField : Hashable {
public let rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
}
public struct Request : Equatable {
public var method: Method = .get
public var components: URLComponents
public var header: [HeaderField: String] = [:]
public var body: Data? = nil
}
public protocol Endpoint {
/// The request describing the reciever.
var request: Request { get }
/// A handler called when a response is recieved from the server
func onResponse(_ response: URLResponse, data: Data) throws
}
public protocol ResourceEndpoint : Endpoint {
/// The type of resource.
associatedtype Resource
/// Creates a Resource value from requested data.
func createResource(from data: Data) throws -> Resource
}
public extension Endpoint {
func onResponse(_ response: URLResponse, data: Data) throws {}
}
public extension ResourceEndpoint where Resource == Data {
func resource(from data: Data) throws -> Resource { data }
}
public class Network {
public typealias Handler<T> = (Result<T, Error>) -> Void
public init(session: URLSession = .shared) {
self.session = session
}
private let session: URLSession
public func fetchResource<T>(at endpoint: T, completion: @escaping Handler<T.Resource>) -> URLSessionDataTask where T : ResourceEndpoint {
fetchData(at: endpoint) { response in
completion(response.flatMap { data in
Result { try endpoint.createResource(from: data) }
})
}
}
public func fetchData(at endpoint: Endpoint, completion: @escaping Handler<Data>) -> URLSessionDataTask {
session.dataTask(with: URLRequest(endpoint.request)) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let response = response {
let data = data ?? Data()
completion(Result(catching: { try endpoint.onResponse(response, data: data) }).map { data })
} else {
fatalError("response, and error are all nil")
}
}
}
}
private extension URLRequest {
init(_ request: Request) {
guard let url = request.components.url else {
fatalError("cannot create url from components: \(request.components)")
}
self.init(url: url)
httpMethod = request.method.rawValue.uppercased()
request.header.forEach { field, value in
addValue(value, forHTTPHeaderField: field.rawValue)
}
httpBody = request.body
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment