Skip to content

surfstudio/NodeKit

 
 

Repository files navigation

Build Status codebeat badge

Core Net Kit

Поинты:

  • Компоненты
  • Низкоуровневая часть и ядро
  • Новый подход
  • Новый интерфейс
  • Набор утилит

Компоненты

  • Core Содержит низкоуровневые компоненты
    • Adapters Протоколы для адаптеров
      • CacheAdapter Адаптеры для кеширования
        • CacheAdapter.swift Протокол для любого кеширующего адаптера
        • UrlCacheAdapter.swift Базовая реализация URL кеша
      • ErrorAdapter Адаптер для маппинга ошибок
        • ErrorMapperAdapter.swift Протокол для маппинга ошибок
    • ServerPart Содержит совсем незкоуровневые вещи
      • CoreServerRequest.swift Собирает, форматирует, отправляет запрос, формирует CoreResponse, сохраняет/читает из кэша
      • ServerRequest+SupportedClasses.swift Вспомогательные классы
      • CoreServerResponse.swift Базовый ответ. Содержит базовую логику обработки сырого ответа
      • MultipartData.swift Класс содержащий данные для Multipart запросов
    • BaseResult.swift Enum представляющий низкоуровневый результат выполнения запроса
    • BaseServerRequet.swift Обертка, реализующая базовую логику форматирования высокоуровневых данных и предоставляющая методы для формирования запроса и обработки ответа конкретных запросов. От него нужно наследоваться для создания собственного запроса
    • NetworkLayerBaseError.swift Базовые ошибки серверного слоя
    • ReusablePagingRequest.swift Протокол для реюза созданного запроса
  • Context Содержит контексты
    • Protocols Протоклы для контекстов
      • ActionableContext.swift Контекст, который инкапсулирует вызов и обработку запроса
      • CacheableContext.swift Контекст, который разделяет успешное выполнение запроса к серверу и успешное выполнение запроса к кешу
      • CancellableContext.swift Контекст, который позволяет отменить запрос
      • HandableContext.swift Контекст, который может получать внутренний обработчик сырого ответа и вызывать его самостоятельно. Можно использовать для конвертирования моделей
      • PagingRequestContext.swift Контекст для пагинации на смещениях
      • PassiveContext.swift Контекст который по сути являетя прокси - обработка происходит где-то в другом месте
    • BaseImplementation Базовые реализации контекстов
      • ActiveRequestContext.swift Реализация ActionableContext
      • HandleRequestContext.swift Реализация HandableContext
      • PagingRequestContext.swift Реализация PagingRequestContext
      • PassiveRequestContext.swift Реализация PassiveContext
    • Kit Набор утилит и хелперов
      • Pagination Файлы с пагинаторами
        • BaseIteratableContext.swift Базовая реализация контекста асинхронного итератора
        • Countable.swift Протокол для коллекций
        • ServicePaginator+AsyncIterator.swift Протоколы для пагинаторов
      • Safe Автоматическая реализация безопасного доступа (с обновлением токена доступа)
        • AccessSafeRequestManager.swift Менеджер для обеспечения обновления токена и переотправки запросов

Низкоуровневая часть и ядро

BaseServerRequest


Инкапсулирует логику для отправки запроса в сеть и инициаллизации всей низкоуровневой логической цепочки.

Пользователю дает возможность конструировать запрос и обрабатывать сырой ответ.

Пример:

import Foundation
import ObjectMapper

class GetOrderListRequest: BaseServerRequest<[OrderMiniEntity]> {

    private struct Keys {
        public static let skipOrdersCount = "skip"
        public static let ordersPerPage = "take"
        public static let orders = "items"
    }

    private let skipOrdersCount: Int
    private let ordersPerPage: Int

    public init(skipOrdersCount: Int, ordersPerPage: Int) {
        self.skipOrdersCount = skipOrdersCount
        self.ordersPerPage = ordersPerPage
    }

    override func createAsyncServerRequest() -> ServerRequest {
        let params = ServerRequestParameter.simpleParams([Keys.skipOrdersCount: self.skipOrdersCount, Keys.ordersPerPage: self.ordersPerPage])
        let request = ServerRequest(method: .get,
                                    relativeUrl: YourURLs.orders,
                                    baseUrl: YourURLs.baseStagingUrl,
                                    token: AuthModel.accessToken,
                                    parameters: params)
        request.cachePolicy = .serverOnly

        return request
    }

    override func handle(serverResponse: ServerResponse, completion: (ResponseResult<[OrderMiniEntity]>) -> Void) {
        let result = {() -> ResponseResult<[OrderMiniEntity]> in
            switch serverResponse.result {
            case .failure(let error):
                    return .failure(error)
            case .success(let value, let  flag):

                guard let json = value as? [String: Any] else {
                    return .failure(BaseServerError.cantMapping)
                }

                guard let ordersJson = json[Keys.orders] else {
                    return .success([OrderMiniEntity](), flag)
                }

                guard let mapped = Mapper<OrderMiniEntity>().mapArray(JSONObject: ordersJson) else {
                    return .failure(BaseServerError.cantMapping)
                }
                return .success(mapped, flag)
            }
        }()
        completion(result)
    }
}

Error Adapter


Позволяет передавать логику маппинга кастомной ошибки на низкий уровень. Таким образом, если вместе с кодом ошибки сервер отдает какое-то тело ответа, то можно его распарсить с помощью этого адаптера и прокинуть вверх.

Пример:

public enum AuthorizationError: LocalizedError {

    case invalidCode

    public var errorDescription: String? {
        switch self {
        case .invalidCode:
            return L10n.wrongPin
        }
    }
}

public class AuthorizationErrorMapperAdapter: ErrorMapperAdapter {

    private struct Keys {
        public static let error = "error"
    }

    private struct Errors {
        public static let invalidCode = "invalid_grant"
    }

    public func map(json: [String: Any]) -> LocalizedError? {
        guard let error = json[Keys.error] as? String else {
            return nil
        }

        switch error {
        case Errors.invalidCode:
            return AuthorizationError.invalidCode
        default:
            return nil
        }
    }
}

Cache Adapter

Позволяет передать кастомнуб логику чтения/записи в кэш. Имеется реализация по-умолчанию для URLCache


Новый подход


До сих пор сервис был своебразной фабрикой запросов, которая обрабатывала ответ от запроса и вызывала callback. Это похоже на фукциональный стиль программирования. К тому же не очень расширяемо.

Новая идея - новый подход. Он основан на идее контекстов. Контекст - объект, который может инкапсулировать следующую логику (иметь следующе ответственности):

  • Инициаллизация запроса
  • Обработка ответа от запроса
  • Конвертирование данных пришедших из запроса в формат, который ожидает владелец
  • Отмена запроса
  • Проброс типизированных ответов на уровень выше с помощью трех видов колбэков:
    • onSuccess(_ model: <Type>)
    • onError(_ error: Error)
    • onCacheSuccess(_ model: <Type>)

Любой сервисный метод возвращает контекст. Далее, презентер распоряжется им по своему усмотрению.

Новый интерфейс

Как было до этого

ExampleService.exampleMethod(param: Type, ... , completion: { ... })

Как теперь

let service = ExampleService()

service.ExampleMethod(param: Type, ...)
    .onSuccess { ...}
    .onError { ... }
    .onCacheSuccess { ... }

Р-р-р-р-реакт без RxSwift (:

В чем плюс: - Больше лаконичности - Больше никаких switch result { ... } - Можно прямо указать метод для обработки: .onError(self.errorHandler) - Проще тестировать (если писать тесты) - За счет декларативности проще изменять реализацию сервисов (Абстракция)

Набор утилит


Пагинация

Пагинатор имет простую базовую реализацию для пагинирования на оффсетах. Он принимает необходимый контекст и самостоятельно хранит состояние смещений от начала списка. На ружу он выставляет следующий интерфейс:

public protocol ServicePaginator {

    associatedtype Model

    func moveNext()

    func reset(to index: Int?)
}

public protocol ServiceAsyncIterator: ServicePaginator {

    var canMoveNext: Bool { get }
}

Таким образом - пользователю нужно просто указать откуда должна начаться пагинация. Далее нужно просто вызывать moveNext() и если выдача закончится, то moveNext вернет либо оишбку либо что-то что определит пользователь.

В качестве расширения для стандартного ServicePaginator можно использовать ServiceAsyncIterator - он выставляет наружу флаг, по которому можно определить - можно ли дальше продолжать пагинацию.

Базовая реализация есть для ServiceAsyncIterator

Менеджер обновления токена


Задача этого объекта - следить за ответами на запросы и если он обнаруживает ответ с 401 ошибкой, то он останавливает все запросы, посылает запрос на обновление токена, в случае успеха - обновляет токен в хранилище, повторяет провалившийся запрос и пропускает все те, которые накопились в очереди. Таким образом запросы не теряются.

Это синглтонный объект, который нужно заводить на все приложение. Все запросы, для которых необходимо такое поведение, нужно передавать этому менеджеру. Внутри себя он сам менеджрит кому отправиться, а кому подождать.

Версионирование

Версии обозначются в формате x.y.z где

  • х мажорный номер версии. Поднимается только в случае мажерных обновлений (изменения в имплементации, добавление новой функциональности)
  • y минорный номер версии. Поднимается только в случае минорных обновлений (изменения в интерфейсах)
  • z минорный номер версии. Поднимается в случае незначительных багфиксов и т.п.