Skip to content

Instantly share code, notes, and snippets.

@VAndrJ
Last active December 4, 2018 14:02
Show Gist options
  • Save VAndrJ/7664f82d5847551541d5e1e53a57ca57 to your computer and use it in GitHub Desktop.
Save VAndrJ/7664f82d5847551541d5e1e53a57ca57 to your computer and use it in GitHub Desktop.
Piece of network module written for Medinternet project.
//
// NetworkEndpoint.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
enum NetworkEndpoint: String {
#if RELEASE
let domainEndpoint = "https://releaseu_rl/"
#else
let domainEndpoint = "https://development_url/"
#endif
case someEndpoint = "some_api_endpoint"
case someOtherEndpoint = "some_other_api_endpoint"
var path: URL {
// MARK: - must instantly crash to detect problem with url path
return URL(string: "\(domainEndpoint)\(self.rawValue)")!
}
var type: RequestType {
switch self {
case .someEndpoint:
return .get
case .someOtherEndpoint:
return .post
}
}
}
//
// NetworkError.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
enum NetworkError: Error {
case internalServerError
case requestError(global: Error)
case responseParsingError
case responseEmptyData
case responseError(code: Int)
case describedError(message: String)
var localizedDescription: String {
switch self {
case .internalServerError:
return "Внутрiшня помилка сервера. Спробуйте пiзнiше."
case .requestError(let globalError):
return globalError.localizedDescription
case .responseParsingError:
return "Помилка даних вiдповiдi сервера. Зверніться в підтримку."
case .responseEmptyData:
return "Порожні дані відповіді сервера."
case .responseError(let code):
return "Помилка вiдповiдi сервера #\(code). Зверніться в підтримку."
case .describedError(let message):
return message
}
}
}
//
// Networking.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
import Bond
import ReactiveKit
final class Networking: NSObject {
let networkService: NetworkService
let logger: NetworkLogProtocol & ParsingLogProtocol
init(logger: NetworkLogProtocol & ParsingLogProtocol) {
self.logger = logger
self.networkService = NetworkService(logger: logger)
}
func getSomeInfo(token: String,
completion: @escaping (SomeResponseEntity) -> Void,
errorCompletion: @escaping (NetworkError) -> Void,
always: (() -> Void)?) {
let request = TokenEntity(date: Date(), token: token)
let resource = Resource<SomeResponseEntity, TokenEntity>(endpoint: .someEndpoint, body: request, logger: logger)
coreRequest(
resourse: resource,
completion: completion,
errorCompletion: errorCompletion,
always: always
)
}
//...other methods
private func coreRequest<Response: Codable, Request: Codable>(
resourse: Resource<Response, Request>,
completion: @escaping (Response) -> Void,
errorCompletion: @escaping (NetworkError) -> Void,
always: (() -> Void)?) {
networkService
.load(resourse)
.observeOn(.main)
.observe(with: { (event) in
switch event {
case .next(let response):
always?()
completion(response)
case .failed(let error):
always?()
errorCompletion(error)
case .completed:
break
}
})
.dispose(in: bag)
}
}
//
// NetworkLog.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
protocol NetworkLogProtocol {
func logResponse(request: URLRequest, data: Data?, response: URLResponse?, error: Error?)
}
protocol ParsingLogProtocol {
func logParsingError(_ error: Error)
}
// MARK: - It's very simple, so use one class
final class NetworkLog: NetworkLogProtocol, ParsingLogProtocol {
let dateFormatter: DateFormatter
init(dateFormatter: DateFormatter) {
self.dateFormatter = dateFormatter
}
func logParsingError(_ error: Error) {
#if DEBUG
let errorString = """
- - - Parsing error start - - -
Time: \(dateFormatter.string(from: Date()))
\(error.localizedDescription)
\(error)
- - - Parsing error end - - -
"""
print(errorString)
#endif
}
func logResponse(request: URLRequest, data: Data?, response: URLResponse?, error: Error?) {
#if DEBUG
let requestDataString = String(data: request.httpBody ?? Data(), encoding: .utf8) ?? ""
let requestString = """
- - - Request start - - -
Time: \(dateFormatter.string(from: Date()))
URL: \(request.url?.absoluteString ?? "error url")
Header: \(request.allHTTPHeaderFields ?? [:])
Method: \(request.httpMethod ?? "")
Body: \(requestDataString)
- - - Request end - - -
"""
let responseDataString = String(data: data ?? Data(), encoding: .utf8) ?? ""
let responseString = """
- - - Response start - - -
Code: \((response as? HTTPURLResponse)?.statusCode ?? -1)
Error: \(error?.localizedDescription ?? "")
Body: \(responseDataString)
- - - Response end - - -
"""
print(requestString)
print(responseString)
#endif
}
}
//
// NetworkService.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import UIKit
import Bond
import ReactiveKit
protocol NetworkServiceProtocol {
func load<A, B>(_ resource: Resource<A, B>) -> Signal<A, NetworkError>
}
final class NetworkService: NetworkServiceProtocol {
private let session = URLSession(configuration: .ephemeral)
private let logger: NetworkLogProtocol?
init(logger: NetworkLogProtocol? = nil) {
self.logger = logger
}
func load<A, B>(_ resource: Resource<A, B>) -> Signal<A, NetworkError> {
return Signal { observer in
var urlRequest = URLRequest(url: resource.endpoint.path, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: Configuration.requestTimeout)
urlRequest.httpMethod = resource.endpoint.type.rawValue
switch resource.endpoint.type {
case .get:
break
case .post:
urlRequest.allHTTPHeaderFields = resource.getHeaders()
if let body = resource.body {
urlRequest.httpBody = body.getFormEncoded()?.data(using: .utf8)
}
}
let task = self.session.dataTask(with: urlRequest) { [weak self] (data, response, error) in
guard let `self` = self else { return }
self.logger?.logResponse(request: urlRequest, data: data, response: response, error: error)
guard error == nil else {
observer.failed(.requestError(global: error!))
return
}
guard self.validateResponse(response) else {
observer.failed(.responseError(code: self.checkResponseCode(response)))
return
}
guard let data = data else {
observer.failed(.responseEmptyData)
return
}
guard let parsedResponse = resource.parse(data) else {
// MARK: - For error response with code 200 ¯\_(ツ)_/¯
guard let errorResponse = resource.parseError(data) else {
observer.failed(.responseParsingError)
return
}
observer.failed(NetworkError.describedError(message: errorResponse.formError?.all ?? errorResponse.error))
return
}
observer.next(parsedResponse)
observer.completed()
}
task.resume()
return BlockDisposable {
task.cancel()
}
}
}
private func validateResponse(_ response: URLResponse?) -> Bool {
guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else {
return false
}
return true
}
private func checkResponseCode(_ response: URLResponse?) -> Int {
// MARK: - For peace of mind
guard let response = response as? HTTPURLResponse else {
return -1
}
return response.statusCode
}
}
//
// RequestType.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
enum RequestType: String {
case post = "POST"
case get = "GET"
}
//
// Resource.swift
// Medinternet
//
// Created by VAndrJ on 6/10/18.
// Copyright © 2018 VAndrJ. All rights reserved.
//
import Foundation
struct Resource<A: Codable, B: Codable & FormEncoded> {
private let logger: ParsingLogProtocol?
private let defaultHeaders: [String: String] = ["Content-Type": "application/x-www-form-urlencoded"]
private let additionalHeaders: [String: String]?
let endpoint: NetworkEndpoint
let body: B?
init(endpoint: NetworkEndpoint, body: B?, additionalHeaders: [String: String]? = nil, logger: ParsingLogProtocol? = nil) {
self.endpoint = endpoint
self.body = body
self.additionalHeaders = additionalHeaders
self.logger = logger
}
func parse(_ data: Data) -> A? {
do {
return try JSONDecoder().decode(A.self, from: data)
} catch {
logger?.logParsingError(error)
return nil
}
}
func parseError(_ data: Data) -> ErrorEntity? {
do {
return try JSONDecoder().decode(ErrorEntity.self, from: data)
} catch {
logger?.logParsingError(error)
return nil
}
}
func getHeaders() -> [String: String]? {
switch endpoint.type {
case .get:
return nil
case .post:
return defaultHeaders.merging(additionalHeaders ?? [:], uniquingKeysWith: { $1 })
}
}
}
@Terens777
Copy link

nice work

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