-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e6102fc
commit 023e441
Showing
25 changed files
with
1,793 additions
and
136 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// | ||
// EtagReaderNode.swift | ||
// CoreNetKit | ||
// | ||
// Created by Александр Кравченков on 05/03/2019. | ||
// Copyright © 2019 Кравченков Александр. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Этот узел читает eTag-токен из хранилища и добавляет его к запросу. | ||
open class URLETagReaderNode: AsyncNode { | ||
|
||
// Следующий узел для обработки. | ||
public var next: any TransportLayerNode | ||
|
||
/// Ключ, по которому необходимо получить eTag-токен из хедеров. | ||
/// По-молчанию имеет значение `eTagRequestHeaderKey` | ||
public var etagHeaderKey: String | ||
|
||
/// Инициаллизирует узел. | ||
/// | ||
/// - Parameters: | ||
/// - next: Следующий узел для обработки. | ||
/// - eTagHeaderKey: Ключ, по которому необходимо добавить eTag-токен к запросу. | ||
public init(next: some TransportLayerNode, | ||
etagHeaderKey: String = ETagConstants.eTagRequestHeaderKey) { | ||
self.next = next | ||
self.etagHeaderKey = etagHeaderKey | ||
} | ||
|
||
/// Пытается прочесть eTag-токен из хранилища и добавить его к запросу. | ||
/// В случае, если прочесть токен не удалось, то управление просто передается дальше. | ||
open func process( | ||
_ data: TransportURLRequest, | ||
logContext: LoggingContextProtocol | ||
) async -> NodeResult<Json> { | ||
guard | ||
let key = data.url.withOrderedQuery(), | ||
let tag = UserDefaults.etagStorage?.value(forKey: key) as? String | ||
else { | ||
return await next.process(data, logContext: logContext) | ||
} | ||
|
||
var headers = data.headers | ||
headers[self.etagHeaderKey] = tag | ||
|
||
let params = TransportURLParameters(method: data.method, | ||
url: data.url, | ||
headers: headers) | ||
|
||
let newData = TransportURLRequest(with: params, raw: data.raw) | ||
|
||
return await next.process(newData, logContext: logContext) | ||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// | ||
// eTagSaverNode.swift | ||
// CoreNetKit | ||
// | ||
// Created by Александр Кравченков on 04/03/2019. | ||
// Copyright © 2019 Кравченков Александр. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
// MARK: - UserDefaults eTag storage | ||
|
||
/// Содержит указатель на UserDefaults-хранилище для eTag токенов. | ||
extension UserDefaults { | ||
/// Хранилище для eTag-токенов | ||
static var etagStorage = UserDefaults(suiteName: "\(String(describing: self.self))") | ||
} | ||
|
||
/// Этот узел сохраняет пришедшие eTag-токены. | ||
/// В качестве ключа используется абсолютный URL до endpoint-a. | ||
open class URLETagSaverNode: AsyncNode { | ||
|
||
/// Следующий узел для обработки. | ||
public var next: (any ResponsePostprocessorLayerNode)? | ||
|
||
/// Ключ, по которому необходимо получить eTag-токен из хедеров. | ||
/// По-молчанию имеет значение `ETagConstants.eTagResponseHeaderKey` | ||
public var eTagHeaderKey: String | ||
|
||
/// Инициаллизирует узел. | ||
/// | ||
/// - Parameters: | ||
/// - next: Следующий узел для обработки. | ||
/// - eTagHeaderKey: Ключ, по которому необходимо получить eTag-токен из хедеров. | ||
public init(next: (any ResponsePostprocessorLayerNode)?, eTagHeaderKey: String = ETagConstants.eTagResponseHeaderKey) { | ||
self.next = next | ||
self.eTagHeaderKey = eTagHeaderKey | ||
} | ||
|
||
/// Пытается получить eTag-токен по ключу. | ||
/// В любом случае передает управление дальше. | ||
open func process( | ||
_ data: URLProcessedResponse, | ||
logContext: LoggingContextProtocol | ||
) async -> NodeResult<Void> { | ||
guard let tag = data.response.allHeaderFields[self.eTagHeaderKey] as? String, | ||
let url = data.request.url, | ||
let urlAsKey = url.withOrderedQuery() | ||
else { | ||
return await next?.process(data, logContext: logContext) ?? .success(()) | ||
} | ||
|
||
UserDefaults.etagStorage?.set(tag, forKey: urlAsKey) | ||
|
||
return await next?.process(data, logContext: logContext) ?? .success(()) | ||
} | ||
} | ||
|
||
public extension URL { | ||
|
||
/// Берет исходный URL | ||
/// Получает словарь параметров query | ||
/// Если параметров нет - возвращает `self.absoluteString` | ||
/// Если параметры есть - сортирует их соединяет в одну строку | ||
/// Удаляет query параметры из исходного URL | ||
/// Склеивает строковое представление URL без парамтеров со сторокой параметров | ||
/// | ||
/// **ВАЖНО** | ||
/// | ||
/// Полученная строка может быть невалидным URL - т.к. задача этого метода - получить уникальный идентификатор из URL | ||
/// Причем так, чтобы порядок перечисления query парамтеров был не важен. | ||
func withOrderedQuery() -> String? { | ||
guard var comp = URLComponents(string: self.absoluteString) else { | ||
return nil | ||
} | ||
|
||
// ели нет query параметров, то просто возвращаем этот url т.к. ничего сортировать не надо | ||
if comp.queryItems == nil || comp.queryItems?.isEmpty == true { | ||
return self.absoluteString | ||
} | ||
|
||
let ordereedQueryString = comp.queryItems! | ||
.map { $0.description } | ||
.sorted() | ||
.reduce("", { $1 + $0 }) | ||
|
||
// если в компонентах сбросить query в nil, то в итоговом URL не будет query | ||
comp.query = nil | ||
|
||
guard let url = comp.url else { | ||
return nil | ||
} | ||
|
||
return url.absoluteString + ordereedQueryString | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// | ||
// URLCacheReaderNode.swift | ||
// CoreNetKitWithExample | ||
// | ||
// Created by Александр Кравченков on 28/11/2018. | ||
// Copyright © 2018 Александр Кравченков. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Ошибки для узла `URLCacheReaderNode` | ||
/// | ||
/// - cantLoadDataFromCache: Случается, если запрос в кэш завершился с ошибкой | ||
/// - cantSerializeJson: Случается, если запрос в кэш завершился успешно, но не удалось сериализовать ответ в JSON | ||
/// - cantCastToJson: Случается, если сериализовать ответ удалось, но каст к `Json` или к `[Json]` завершился с ошибкой | ||
public enum BaseURLCacheReaderError: Error { | ||
case cantLoadDataFromCache | ||
case cantSerializeJson | ||
case cantCastToJson | ||
} | ||
|
||
/// Этот узел отвечает за чтение данных из URL кэша. | ||
/// Сам по себе узел является листом и не может быть встроен в сквозную цепочку. | ||
open class URLCacheReaderNode: AsyncNode { | ||
|
||
public var needsToThrowError: Bool | ||
|
||
public init(needsToThrowError: Bool) { | ||
self.needsToThrowError = needsToThrowError | ||
} | ||
|
||
/// Посылает запрос в кэш и пытается сериализовать данные в JSON. | ||
open func process( | ||
_ data: URLNetworkRequest, | ||
logContext: LoggingContextProtocol | ||
) async -> NodeResult<Json> { | ||
guard let cachedResponse = extractCachedURLResponse(data.urlRequest) else { | ||
return .failure(BaseURLCacheReaderError.cantLoadDataFromCache) | ||
} | ||
|
||
guard let jsonObjsect = try? JSONSerialization.jsonObject( | ||
with: cachedResponse.data, | ||
options: .allowFragments | ||
) else { | ||
return .failure(BaseURLCacheReaderError.cantSerializeJson) | ||
} | ||
|
||
guard let json = jsonObjsect as? Json else { | ||
guard let json = jsonObjsect as? [Json] else { | ||
return .failure(BaseURLCacheReaderError.cantCastToJson) | ||
} | ||
return .success([MappingUtils.arrayJsonKey: json]) | ||
} | ||
|
||
return .success(json) | ||
} | ||
|
||
private func extractCachedURLResponse(_ request: URLRequest) -> CachedURLResponse? { | ||
if let response = URLCache.shared.cachedResponse(for: request) { | ||
return response | ||
} | ||
return nil | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// URLCacheWriterNode.swift | ||
// CoreNetKitWithExample | ||
// | ||
// Created by Александр Кравченков on 28/11/2018. | ||
// Copyright © 2018 Александр Кравченков. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Этот узел занимается записью данных в URL кэш. | ||
/// - Important: это "глупая" реализация, | ||
/// в которой не учитываются server-side политики и прочее. | ||
/// Подразумечается, что этот узел не входит в цепочку, а является листом одного из узлов. | ||
open class URLCacheWriterNode: AsyncNode { | ||
|
||
/// Формирует `CachedURLResponse` с политикой `.allowed`, сохраняет его в кэш, | ||
/// а затем возвращает сообщение об успешной операции. | ||
open func process( | ||
_ data: URLProcessedResponse, | ||
logContext: LoggingContextProtocol | ||
) async -> NodeResult<Void> { | ||
let cached = CachedURLResponse( | ||
response: data.response, | ||
data: data.data, | ||
storagePolicy: .allowed | ||
) | ||
URLCache.shared.storeCachedResponse(cached, for: data.request) | ||
return .success(()) | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// | ||
// ETagRederNode.swift | ||
// CoreNetKit | ||
// | ||
// Created by Александр Кравченков on 04/03/2019. | ||
// Copyright © 2019 Кравченков Александр. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Этот узел проверяет код ответа от сервера и в случае, если код равен 304 (NotModified) | ||
/// Узел посылает запрос в URL кэш. | ||
open class URLNotModifiedTriggerNode: AsyncNode { | ||
|
||
// MARK: - Properties | ||
|
||
/// Следующий узел для обратки. | ||
public var next: any ResponseProcessingLayerNode | ||
|
||
/// Узел для чтения данных из кэша. | ||
public var cacheReader: any AsyncNode<URLNetworkRequest, Json> | ||
|
||
// MARK: - Init and deinit | ||
|
||
/// Инициаллизирует узел. | ||
/// | ||
/// - Parameters: | ||
/// - next: Следующий узел для обратки. | ||
/// - cacheReader: Узел для чтения данных из кэша. | ||
public init(next: some ResponseProcessingLayerNode, | ||
cacheReader: some AsyncNode<URLNetworkRequest, Json>) { | ||
self.next = next | ||
self.cacheReader = cacheReader | ||
} | ||
|
||
// MARK: - Node | ||
|
||
/// Проверяет http status-code. Если код соовуетствует NotModified, то возвращает запрос из кэша. | ||
/// В протвином случае передает управление дальше. | ||
open func process( | ||
_ data: URLDataResponse, | ||
logContext: LoggingContextProtocol | ||
) async -> NodeResult<Json> { | ||
guard data.response.statusCode == 304 else { | ||
await logContext.add(makeErrorLog(code: data.response.statusCode)) | ||
return await next.process(data, logContext: logContext) | ||
} | ||
|
||
await logContext.add(makeSuccessLog()) | ||
|
||
return await cacheReader.process( | ||
URLNetworkRequest(urlRequest: data.request), | ||
logContext: logContext | ||
) | ||
} | ||
|
||
// MARK: - Private Methods | ||
|
||
private func makeErrorLog(code: Int) -> Log { | ||
let msg = "Response status code = \(code) != 304 -> skip cache reading" | ||
return Log( | ||
logViewObjectName + msg, | ||
id: objectName | ||
) | ||
} | ||
|
||
private func makeSuccessLog() -> Log { | ||
let msg = "Response status code == 304 -> read cache" | ||
return Log( | ||
logViewObjectName + msg, | ||
id: objectName | ||
) | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Foundation | ||
|
||
/// Модель данных для конфигурироания цепочки преобразований для запроса в сеть. | ||
public struct URLChainConfigModel { | ||
|
||
/// HTTP метод, который будет использован цепочкой | ||
public let method: Method | ||
|
||
/// Маршрут до удаленного метода (в частном случае - URL endpoint'a) | ||
public let route: URLRouteProvider | ||
|
||
/// В случае классического HTTP это Header'ы запроса. | ||
/// По-умолчанию пустой. | ||
public let metadata: [String: String] | ||
|
||
/// Кодировка данных для запроса. | ||
/// По умолчанию`.json` | ||
public let encoding: ParametersEncoding | ||
|
||
/// Инициаллизирует объект. | ||
/// | ||
/// - Parameters: | ||
/// - method: HTTP метод, который будет использован цепочкой | ||
/// - route: Маршрут до удаленного метод | ||
/// - metadata: В случае классического HTTP это Header'ы запроса. По-умолчанию пустой. | ||
/// - encoding: Кодировка данных для запроса. По-умолчанию `.json` | ||
public init(method: Method, | ||
route: URLRouteProvider, | ||
metadata: [String: String] = [:], | ||
encoding: ParametersEncoding = .json) { | ||
self.method = method | ||
self.route = route | ||
self.metadata = metadata | ||
self.encoding = encoding | ||
} | ||
} |
Oops, something went wrong.