Last active
October 19, 2019 14:49
-
-
Save KaQuMiQ/4e87d82d447b044cdd162325489158d0 to your computer and use it in GitHub Desktop.
Declarative network client in swift
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 | |
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 {} |
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 | |
internal enum NetworkClientError: Error { | |
case invalidURL | |
case invalidURLQuery | |
case invalidResponse(URLResponse, Data?) | |
case unableToDecodeResponse(URLResponse, Data, Error) | |
case invalidState | |
case sessionClosed | |
} |
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 | |
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() } | |
} |
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
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() {} | |
} |
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
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) | |
} | |
} |
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 | |
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) } } ) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example: