Skip to content

Instantly share code, notes, and snippets.

@KaQuMiQ
Last active October 19, 2019 14:49
Show Gist options
  • Save KaQuMiQ/4e87d82d447b044cdd162325489158d0 to your computer and use it in GitHub Desktop.
Save KaQuMiQ/4e87d82d447b044cdd162325489158d0 to your computer and use it in GitHub Desktop.
Declarative network client in swift
import Foundation
public protocol DataEncoder {
func encode<T>(_ value: T) throws -> Data where T: Encodable
}
extension JSONEncoder: DataEncoder {}
public protocol DataDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable
}
extension JSONDecoder: DataDecoder {}
import Foundation
internal enum NetworkClientError: Error {
case invalidURL
case invalidURLQuery
case invalidResponse(URLResponse, Data?)
case unableToDecodeResponse(URLResponse, Data, Error)
case invalidState
case sessionClosed
}
import Foundation
fileprivate let jsonEncoder: JSONEncoder = .init()
fileprivate let jsonDecoder: JSONDecoder = .init()
public enum HTTPMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
case delete = "DELETE"
}
public protocol NetworkEndpoint {
associatedtype Session: NetworkSession
associatedtype Request: NetworkEndpointRequest where Request.Endpoint == Self
associatedtype Response
static var urlTemplate: String { get }
static var method: HTTPMethod { get }
static var timeout: TimeInterval { get }
static var baseHeaders: [String: String] { get }
static var encoder: DataEncoder { get }
static var decoder: DataDecoder { get }
static func url(for request: Request, in session: Session) throws -> URL
static func urlQuery(for request: Request, in session: Session) -> [URLQueryItem]
static func headers(for request: Request, in session: Session) -> [String: String]
static func bodyData(for request: Request, in session: Session) throws -> Data?
static func request(with request: Request, in session: Session) throws -> URLRequest
static func response(from response: URLResponse, data: Data?, in session: Session) throws -> Response
}
extension NetworkEndpoint {
public static var timeout: TimeInterval { 60 }
public static var baseHeaders: [String: String] { return [:] }
public static var encoder: DataEncoder { jsonEncoder }
public static var decoder: DataDecoder { jsonDecoder }
public static func url(for _: Request, in _: Session) throws -> URL {
guard let url = URL(string: urlTemplate) else {
throw NetworkClientError.invalidURL
}
return url
}
public static func urlQuery(for _: Request, in _: Session) -> [URLQueryItem] { [] }
public static func headers(for _: Request, in _: Session) -> [String: String] { [:] }
public static func request(with request: Request, in session: Session) throws -> URLRequest {
var requestURL = try url(for: request, in: session)
let query = urlQuery(for: request, in: session)
if !query.isEmpty {
guard var components = URLComponents(url: requestURL, resolvingAgainstBaseURL: true) else {
throw NetworkClientError.invalidURL
}
components.queryItems = query
guard let url = components.url else {
throw NetworkClientError.invalidURLQuery
}
requestURL = url
} else { /* nothing */ }
var urlRequest: URLRequest = .init(url: requestURL)
urlRequest.timeoutInterval = timeout
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = baseHeaders.merging(headers(for: request, in: session), uniquingKeysWith: { _, right in right })
urlRequest.httpBody = try bodyData(for: request, in: session)
return urlRequest
}
}
extension NetworkEndpoint where Request.Body: Encodable {
public static func bodyData(for request: Request, in _: Session) throws -> Data? {
return try encoder.encode(request.body)
}
}
extension NetworkEndpoint where Response: Decodable {
public static func response(from response: URLResponse, data: Data?, in _: Session) throws -> Response {
guard let responseData = data else {
throw NetworkClientError.invalidResponse(response, data)
}
do {
return try decoder.decode(Response.self, from: responseData)
} catch {
throw NetworkClientError.unableToDecodeResponse(response, responseData, error)
}
}
}
extension NetworkEndpoint where Request.Body == Void {
public static func bodyData(for _: Request, in _: Session) throws -> Data? { return nil }
}
extension NetworkEndpoint where Response == Void {
public static func response(from _: URLResponse, data _: Data?, in _: Session) throws -> Response { Void() }
}
public protocol NetworkEndpointRequest {
associatedtype Body
associatedtype Endpoint: NetworkEndpoint
var body: Body { get }
}
extension NetworkEndpointRequest where Body == Void {
public var body: Body { Void() }
}
public protocol EmptyNetworkEndpointRequest: NetworkEndpointRequest where Body == Void {
init()
}
public struct EmptyNetworkRequest<Endpoint: NetworkEndpoint>: EmptyNetworkEndpointRequest {
public init() {}
}
public protocol NetworkSession {
func call<Endpoint: NetworkEndpoint>(_ endpoint: Endpoint.Type, with request: Endpoint.Request, _ completion: @escaping (Result<Endpoint.Response, Error>) -> Void) throws where Endpoint.Session == Self
}
public extension NetworkSession {
func call<Endpoint: NetworkEndpoint>(_ endpoint: Endpoint.Type, completion: @escaping (Result<Endpoint.Response, Error>) -> Void) throws where Endpoint.Request: EmptyNetworkEndpointRequest, Endpoint.Session == Self {
try call(endpoint, with: .init(), completion)
}
}
import Foundation
public final class SimpleNetworkSession: NetworkSession {
private let makeRequest: (URLRequest, @escaping (Result<(response: HTTPURLResponse, data: Data?), Error>) -> Void) -> Void
public init(makeRequest: @escaping (URLRequest, @escaping (Result<(response: HTTPURLResponse, data: Data?), Error>) -> Void) -> Void) {
self.makeRequest = makeRequest
}
public init() {
self.makeRequest
= { request, completion in
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let response = response as? HTTPURLResponse {
completion(.success((response, data)))
} else {
completion(.failure(NetworkClientError.invalidState))
}
}.resume()
}
}
public func call<Endpoint: NetworkEndpoint>(_ endpoint: Endpoint.Type, with request: Endpoint.Request, _ completion: @escaping (Result<Endpoint.Response, Error>) -> Void) throws where Endpoint.Session == SimpleNetworkSession {
let request = try endpoint.request(with: request, in: self)
makeRequest(request) { [weak self] result in
guard let self = self else { return completion(.failure(NetworkClientError.sessionClosed))}
completion(result.flatMap { result in Result { try endpoint.response(from: result.response, data: result.data, in: self) } } )
}
}
}
@KaQuMiQ
Copy link
Author

KaQuMiQ commented Oct 14, 2019

Example:

// definition
internal enum SomeEndpoint: NetworkEndpoint {
  static var urlTemplate: String { "https://www.apple.com" }
  static var method: HTTPMethod { .get }
  typealias Session = SimpleNetworkSession
  typealias Request = EmptyNetworkRequest<Self>
  typealias Response = Void
}

// and then usage:
let session = SimpleNetworkSession()
try session.call(SomeEndpoint.self) { (result) in
  // handle result
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment