Skip to content

Commit

Permalink
SPT-1998 add merge node
Browse files Browse the repository at this point in the history
  • Loading branch information
mrandrewsmith committed May 7, 2024
1 parent ba2e35d commit dee8cc5
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 58 deletions.
4 changes: 4 additions & 0 deletions NodeKit/NodeKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
5097DFAE2BCDFA8300D422EE /* NodeKitThirdParty in Frameworks */ = {isa = PBXBuildFile; productRef = 5097DFAD2BCDFA8300D422EE /* NodeKitThirdParty */; };
5097DFB02BCDFB5200D422EE /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5097DFAF2BCDFB5200D422EE /* CancellableTask.swift */; };
50B6838F2BBF3615001F7EA3 /* AccessSafeNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B6838E2BBF3615001F7EA3 /* AccessSafeNodeTests.swift */; };
50BEF7BE2BE8A713003DCB04 /* MergedAsyncStreamNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BEF7BD2BE8A713003DCB04 /* MergedAsyncStreamNode.swift */; };
50C57AB72BE3871D004C344E /* ServiceChainProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C57AB62BE3871D004C344E /* ServiceChainProvider.swift */; };
50C57AB92BE388C6004C344E /* ChainBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C57AB82BE388C6004C344E /* ChainBuilder.swift */; };
50C8EB282BBD7A2200C5CB93 /* AsyncStreamCombineNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C8EB272BBD7A2200C5CB93 /* AsyncStreamCombineNode.swift */; };
Expand Down Expand Up @@ -358,6 +359,7 @@
5097DF452BCD628300D422EE /* AsyncPagerDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncPagerDataProvider.swift; sourceTree = "<group>"; };
5097DFAF2BCDFB5200D422EE /* CancellableTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancellableTask.swift; sourceTree = "<group>"; };
50B6838E2BBF3615001F7EA3 /* AccessSafeNodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessSafeNodeTests.swift; sourceTree = "<group>"; };
50BEF7BD2BE8A713003DCB04 /* MergedAsyncStreamNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergedAsyncStreamNode.swift; sourceTree = "<group>"; };
50C57AB62BE3871D004C344E /* ServiceChainProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceChainProvider.swift; sourceTree = "<group>"; };
50C57AB82BE388C6004C344E /* ChainBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainBuilder.swift; sourceTree = "<group>"; };
50C8EB272BBD7A2200C5CB93 /* AsyncStreamCombineNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStreamCombineNode.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -514,6 +516,7 @@
501349142BE3E6D2002CC3DA /* AnyAsyncNode.swift */,
5005EB222BB8A6D900B670CD /* AsyncStreamNode.swift */,
501349162BE3E770002CC3DA /* AnyAsyncStreamNode.swift */,
50BEF7BD2BE8A713003DCB04 /* MergedAsyncStreamNode.swift */,
);
path = Async;
sourceTree = "<group>";
Expand Down Expand Up @@ -1481,6 +1484,7 @@
50C8EB2A2BBD7DEA00C5CB93 /* CombineNode.swift in Sources */,
90B609A5283E1287006F4309 /* ResponseProcessorNode.swift in Sources */,
90B60995283E1287006F4309 /* DTOEncoderNode.swift in Sources */,
50BEF7BE2BE8A713003DCB04 /* MergedAsyncStreamNode.swift in Sources */,
90B60914283E1268006F4309 /* ETagConstants.swift in Sources */,
50528E292BAE162600E86CB6 /* LoggerStreamNode.swift in Sources */,
90B609A2283E1287006F4309 /* ResponseDataParserNode.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion NodeKit/NodeKit/CacheNode/FirstCachePolicyNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum BaseFirstCachePolicyNodeError: Error {

/// Этот узел реализует политику кэширования
/// "Сначала читаем из кэша, а затем запрашиваем у сервера"
/// - Important: В ообщем случае слушатель может быть оповещен дважды. Первый раз, когда ответ прочитан из кэша, а второй раз, когда он был получен с сервера.
/// - Important: В общем случае слушатель может быть оповещен дважды. Первый раз, когда ответ прочитан из кэша, а второй раз, когда он был получен с сервера.
open class FirstCachePolicyNode: AsyncStreamNode {
// MARK: - Nested

Expand Down
100 changes: 53 additions & 47 deletions NodeKit/NodeKit/Chains/ChainBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,41 +92,47 @@ open class URLChainBuilder<Route: URLRouteProvider>: ChainConfigBuilder, ChainBu

// MARK: - Public Methods

/// Добавляет к цепочке RequestRouterNode на основе заданного Route.
/// Если Route не был задан до вызова метода, будет выброшена ошибка.
open func requestRouterNode<Raw, Output>(
next: some AsyncNode<RoutableRequestModel<URLRouteProvider, Raw>, Output>
root: some AsyncNode<RoutableRequestModel<URLRouteProvider, Raw>, Output>
) -> RequestRouterNode<Raw, URLRouteProvider, Output> {
guard let route else {
preconditionFailure("\(self.self) URLRoute is nil")
}

return RequestRouterNode(next: next, route: route)
return RequestRouterNode(next: root, route: route)
}

/// Создает цепочку узлов, описывающих слой построения запроса.
/// Добавляет к цепочке root цепочку узлов, описывающих слой построения запроса.
/// Используется для запросов, ожидающих Json или Data в ответ.
///
/// - Parameter config: Конфигурация для запроса
open func metadataConnectorNode<O>(
/// - Parameter root: Цепочка, к которой будут добавлены узлы
open func metadataConnectorChain<O>(
root: any AsyncNode<TransportURLRequest, O>
) -> some AsyncNode<Json, O> {
let urlRequestEncodingNode = URLJsonRequestEncodingNode(next: root)
let urlRequestTrasformatorNode = URLRequestTrasformatorNode(next: urlRequestEncodingNode, method: method)
let requestEncoderNode = RequestEncoderNode(next: urlRequestTrasformatorNode, encoding: encoding)
let queryInjector = URLQueryInjectorNode(next: requestEncoderNode, config: config)
let requestRouterNode = requestRouterNode(next: queryInjector)
let queryInjectorNode = URLQueryInjectorNode(next: requestEncoderNode, config: config)
let requestRouterNode = requestRouterNode(root: queryInjectorNode)
return MetadataConnectorNode(next: requestRouterNode, metadata: metadata)
}

/// Создает цепочку узлов, описывающих слой построения запроса.
/// Добавляет к цепочке root цепочку узлов, описывающих слой построения запроса.
/// Используется для Multipart запросов.
///
/// - Parameter config: Конфигурация для запроса
open func metadataConnectorNode(
root: any AsyncNode<URLRequest, Json>
/// - Parameter root: Цепочка, к которой будут добавлены узлы
open func metadataConnectorChain(
root: any AsyncNode<MultipartURLRequest, Json>
) -> any AsyncNode<MultipartModel<[String : Data]>, Json> {
let creator = MultipartRequestCreatorNode(next: root)
let transformator = MultipartURLRequestTrasformatorNode(next: creator, method: method)
let queryInjector = URLQueryInjectorNode(next: transformator, config: config)
let router = requestRouterNode(next: queryInjector)
return MetadataConnectorNode(next: router, metadata: metadata)
let requestTransformatorNode = MultipartURLRequestTrasformatorNode(
next: root,
method: method
)
let queryInjectorNode = URLQueryInjectorNode(next: requestTransformatorNode, config: config)
let routerNode = requestRouterNode(root: queryInjectorNode)
return MetadataConnectorNode(next: routerNode, metadata: metadata)
}

// MARK: - ChainConfigBuilder
Expand Down Expand Up @@ -176,65 +182,65 @@ open class URLChainBuilder<Route: URLRouteProvider>: ChainConfigBuilder, ChainBu

open func build<I: DTOEncodable, O: DTODecodable>() -> AnyAsyncNode<I, O>
where I.DTO.Raw == Json, O.DTO.Raw == Json {
let root = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let connector = metadataConnectorNode(root: root)
let dtoConverter = DTOMapperNode<I.DTO, O.DTO>(next: connector)
let modelInputNode = ModelInputNode<I, O>(next: dtoConverter)
let requestChain = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let dtoConverterNode = DTOMapperNode<I.DTO, O.DTO>(next: metadataConnectorChain)
let modelInputNode = ModelInputNode<I, O>(next: dtoConverterNode)
return LoggerNode(next: modelInputNode, filters: logFilter)
.eraseToAnyNode()
}

open func build<O: DTODecodable>() -> AnyAsyncNode<Void, O>
where O.DTO.Raw == Json {
let root = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorNode = metadataConnectorNode(root: root)
let dtoConverter = DTOMapperNode<Json, O.DTO>(next: metadataConnectorNode)
let modelInput = ModelInputNode<Json, O>(next: dtoConverter)
let voidNode = VoidInputNode(next: modelInput)
let requestChain = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let dtoConverterNode = DTOMapperNode<Json, O.DTO>(next: metadataConnectorChain)
let modelInputNode = ModelInputNode<Json, O>(next: dtoConverterNode)
let voidNode = VoidInputNode(next: modelInputNode)
return LoggerNode(next: voidNode, filters: logFilter)
.eraseToAnyNode()
}

open func build<I: DTOEncodable>() -> AnyAsyncNode<I, Void> where I.DTO.Raw == Json {
let root = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorNode = metadataConnectorNode(root: root)
let voidOutput = VoidOutputNode<I>(next: metadataConnectorNode)
return LoggerNode(next: voidOutput, filters: logFilter)
let requestChain = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let voidOutputNode = VoidOutputNode<I>(next: metadataConnectorChain)
return LoggerNode(next: voidOutputNode, filters: logFilter)
.eraseToAnyNode()
}

open func build() -> AnyAsyncNode<Void, Void> {
let root = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorNode = metadataConnectorNode(root: root)
let voidOutput = VoidIONode(next: metadataConnectorNode)
return LoggerNode(next: voidOutput, filters: logFilter)
let requestChain = serviceChainProvider.provideRequestJsonChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let voidOutputNode = VoidIONode(next: metadataConnectorChain)
return LoggerNode(next: voidOutputNode, filters: logFilter)
.eraseToAnyNode()
}

open func build<I: DTOEncodable, O: DTODecodable>() -> AnyAsyncNode<I, O>
where O.DTO.Raw == Json, I.DTO.Raw == MultipartModel<[String : Data]> {
let root = serviceChainProvider.provideRequestMultipartChain()
let metadataConnectorNode = metadataConnectorNode(root: root)
let rawEncoder = DTOMapperNode<I.DTO,O.DTO>(next: metadataConnectorNode)
let dtoEncoder = ModelInputNode<I, O>(next: rawEncoder)
return LoggerNode(next: dtoEncoder, filters: logFilter)
let requestChain = serviceChainProvider.provideRequestMultipartChain()
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let rawEncoderNode = DTOMapperNode<I.DTO,O.DTO>(next: metadataConnectorChain)
let dtoEncoderNode = ModelInputNode<I, O>(next: rawEncoderNode)
return LoggerNode(next: dtoEncoderNode, filters: logFilter)
.eraseToAnyNode()
}

open func buildDataLoading() -> AnyAsyncNode<Void, Data> {
let root = serviceChainProvider.provideRequestDataChain(with: headersProviders)
let metadataConnectorNode = metadataConnectorNode(root: root)
let voidInput = VoidInputNode(next: metadataConnectorNode)
return LoggerNode(next: voidInput, filters: logFilter)
let requestChain = serviceChainProvider.provideRequestDataChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let voidInputNode = VoidInputNode(next: metadataConnectorChain)
return LoggerNode(next: voidInputNode, filters: logFilter)
.eraseToAnyNode()
}

open func buildDataLoading<I: DTOEncodable>() -> AnyAsyncNode<I, Data> where I.DTO.Raw == Json {
let root = serviceChainProvider.provideRequestDataChain(with: headersProviders)
let metadataConnectorNode = metadataConnectorNode(root: root)
let rawEncoder = RawEncoderNode<I.DTO, Data>(next: metadataConnectorNode)
let dtoEncoder = DTOEncoderNode<I, Data>(rawEncodable: rawEncoder)
return LoggerNode(next: dtoEncoder, filters: logFilter)
let requestChain = serviceChainProvider.provideRequestDataChain(with: headersProviders)
let metadataConnectorChain = metadataConnectorChain(root: requestChain)
let rawEncoderNode = RawEncoderNode<I.DTO, Data>(next: metadataConnectorChain)
let dtoEncoderNode = DTOEncoderNode<I, Data>(rawEncodable: rawEncoderNode)
return LoggerNode(next: dtoEncoderNode, filters: logFilter)
.eraseToAnyNode()
}
}
30 changes: 26 additions & 4 deletions NodeKit/NodeKit/Chains/ServiceChainProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public protocol ServiceChainProvider {
with providers: [MetadataProvider]
) -> any AsyncNode<TransportURLRequest, Data>

func provideRequestMultipartChain() -> any AsyncNode<URLRequest, Json>
func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json>
}

open class URLServiceChainProvider: ServiceChainProvider {
Expand All @@ -34,13 +34,17 @@ open class URLServiceChainProvider: ServiceChainProvider {

// MARK: - ServiceChainProvider

/// Цепочка обработки Json ответа от сервера.
open func provideResponseJsonChain() -> any AsyncNode<NodeDataResponse, Json> {
let responseDataParserNode = ResponseDataParserNode()
let responseDataPreprocessorNode = ResponseDataPreprocessorNode(next: responseDataParserNode)
let responseHttpErrorProcessorNode = ResponseHttpErrorProcessorNode(next: responseDataPreprocessorNode)
return ResponseProcessorNode(next: responseHttpErrorProcessorNode)
}

/// Цепочка создания и отправки запроса, ожидающая Json ответ.
///
/// - Parameter providers: Массив провайдеров, предоставляющих метаданные, которые будут включены в запрос.
open func provideRequestJsonChain(
with providers: [MetadataProvider]
) -> any AsyncNode<TransportURLRequest, Json> {
Expand All @@ -53,12 +57,16 @@ open class URLServiceChainProvider: ServiceChainProvider {
return RequestCreatorNode(next: aborterNode, providers: providers)
}

/// Цепочка обработки Data ответа от сервера.
open func provideResponseDataChain() -> any AsyncNode<NodeDataResponse, Data> {
let loaderParser = DataLoadingResponseProcessor()
let errorProcessor = ResponseHttpErrorProcessorNode(next: loaderParser)
return ResponseProcessorNode(next: errorProcessor)
}

/// Цепочка создания и отправки запроса, ожидающая Data ответ.
///
/// - Parameter providers: Массив провайдеров, предоставляющих метаданные, которые будут включены в запрос.
open func provideRequestDataChain(
with providers: [MetadataProvider]
) -> any AsyncNode<TransportURLRequest, Data> {
Expand All @@ -70,8 +78,22 @@ open class URLServiceChainProvider: ServiceChainProvider {
return RequestCreatorNode(next: aborterNode, providers: providers)
}

open func provideRequestMultipartChain() -> any AsyncNode<URLRequest, Json> {
let responseChain = provideResponseJsonChain()
return RequestSenderNode(rawResponseProcessor: responseChain, manager: session)
/// Цепочка обработки Multipart ответа от сервера.
open func provideResponseMultipartChain() -> any AsyncNode<NodeDataResponse, Json> {
let responseDataParserNode = ResponseDataParserNode()
let responseDataPreprocessorNode = ResponseDataPreprocessorNode(next: responseDataParserNode)
let responseHttpErrorProcessorNode = ResponseHttpErrorProcessorNode(next: responseDataPreprocessorNode)
return ResponseProcessorNode(next: responseHttpErrorProcessorNode)
}

/// Цепочка создания и отправки запроса, ожидающая Multipart ответ.
open func provideRequestMultipartChain() -> any AsyncNode<MultipartURLRequest, Json> {
let responseChain = provideResponseMultipartChain()
let requestSenderNode = RequestSenderNode(
rawResponseProcessor: responseChain,
manager: session
)
let aborterNode = AborterNode(next: requestSenderNode, aborter: requestSenderNode)
return MultipartRequestCreatorNode(next: aborterNode)
}
}
14 changes: 14 additions & 0 deletions NodeKit/NodeKit/Core/Node/Async/AsyncNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public protocol AsyncNode<Input, Output>: Node {
///
/// - Returns: Cтруктура-обертку текущей ноды ``AnyAsyncNode``.
func eraseToAnyNode() -> AnyAsyncNode<Input, Output>

/// Метод, позволяющий объединить две ноды с одинаковыми Input и Output в AsyncStreamNode.
///
/// - Parameter node: Нода, необходимая для объединения.
/// - Returns: Нода AsyncStreamNode, включающая текущую и переданную ноду.
func merged(with node: any AsyncNode<Input, Output>) -> any AsyncStreamNode<Input, Output>
}

public extension AsyncNode {
Expand All @@ -56,6 +62,14 @@ public extension AsyncNode {
func eraseToAnyNode() -> AnyAsyncNode<Input, Output> {
return AnyAsyncNode(node: self)
}

/// Стандартная реализация объединения двух узлов в AsyncStreamNode.
///
/// - Parameter node: Нода, необходимая для объединения.
/// - Returns: Нода AsyncStreamNode, включающая текущую и переданную ноду.
func merged(with node: any AsyncNode<Input, Output>) -> any AsyncStreamNode<Input, Output> {
return MergedAsyncStreamNode(firstNode: self, secondNode: node)
}
}

/// Содержит синтаксический сахар для работы с узлами, у которых входящий тип = `Void`
Expand Down
Loading

0 comments on commit dee8cc5

Please sign in to comment.