From 6358386303fa1902f6c6a1c23bd2f2376dcf5bd2 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 19 May 2023 16:30:37 +0300 Subject: [PATCH] feat: added HTTP status codes to `EndpointErrorResult.apiError` responses --- CHANGELOG.md | 4 +++ .../Sources/BaseCancellable.swift | 2 ++ .../Cancellables/Sources/Cancellables.swift | 14 +++++--- .../DefaultJsonNetworkService.swift | 18 +++++----- .../Sources/ApiInteractor/ApiInteractor.swift | 13 +++---- .../ApiInteractor/EndpointErrorResult.swift | 2 +- .../Response/ResponseType+Decoding.swift | 16 ++++----- .../Sources/Response/StatusCodeMimeType.swift | 35 +++++++++++++++++++ .../Response/StatusCodesMimeType.swift | 35 +++++++++++++++++++ .../Sources/Alerts/Models/AlertAction.swift | 6 ++-- .../Alerts/Models/AlertDescriptor.swift | 10 +++--- 11 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 TINetworking/Sources/Response/StatusCodeMimeType.swift create mode 100644 TINetworking/Sources/Response/StatusCodesMimeType.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd750d0..39272316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 1.44.0 + +- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses + ### 1.43.1 - **Fixed**: build scripts submodule url diff --git a/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift index 9797e918..850fa217 100644 --- a/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift +++ b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift @@ -23,6 +23,8 @@ open class BaseCancellable: Cancellable { private(set) public var isCancelled = false + public init() {} + open func cancel() { isCancelled = true } diff --git a/TIFoundationUtils/Cancellables/Sources/Cancellables.swift b/TIFoundationUtils/Cancellables/Sources/Cancellables.swift index cf662966..99b012ee 100644 --- a/TIFoundationUtils/Cancellables/Sources/Cancellables.swift +++ b/TIFoundationUtils/Cancellables/Sources/Cancellables.swift @@ -20,7 +20,7 @@ // THE SOFTWARE. // -public struct Cancellables { +public enum Cancellables { public static func nonCancellable() -> Cancellable { NonCancellable() } @@ -28,21 +28,27 @@ public struct Cancellables { public static func scoped(scopeCancellableClosure: ScopeCancellable.ScopeCancellableClosure) -> Cancellable { ScopeCancellable(scopeCancellableClosure: scopeCancellableClosure) } + + public static func weakTargetClosure(target: T?, + cancelClosure: @escaping WeakTargetCancellable.CancelClosure) -> Cancellable { + + WeakTargetCancellable(target: target, cancelClosure: cancelClosure) + } } @available(iOS 13.0.0, *) public func withTaskCancellableClosure(closure: (@escaping (T) -> Void) -> Cancellable) async -> T { let cancellableBag = BaseCancellableBag() - return await withTaskCancellationHandler(handler: { - cancellableBag.cancel() - }, operation: { + return await withTaskCancellationHandler(operation: { await withCheckedContinuation { continuation in closure { continuation.resume(returning: $0) } .add(to: cancellableBag) } + }, onCancel: { + cancellableBag.cancel() }) } diff --git a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift index 16860ff6..f7da59ff 100644 --- a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift @@ -68,9 +68,9 @@ open class DefaultJsonNetworkService: ApiInteractor { defaultServer: openApi.defaultServer) } - open func process(request: EndpointRequest, + open func process(request: EndpointRequest, mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, + mapFailure: @escaping Closure, R>, mapNetworkError: @escaping Closure, completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { @@ -115,9 +115,9 @@ open class DefaultJsonNetworkService: ApiInteractor { return cancellableBag } - open func process(request: SerializedRequest, + open func process(request: SerializedRequest, mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, + mapFailure: @escaping Closure, R>, mapNetworkError: @escaping Closure, completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { @@ -152,8 +152,10 @@ open class DefaultJsonNetworkService: ApiInteractor { } let decodeResult = rawResponse.decode(mapping: [ - ((successStatusCodes, CommonMediaTypes.applicationJson.rawValue), jsonDecoder.decoding(to: mapSuccess)), - ((failureStatusCodes, CommonMediaTypes.applicationJson.rawValue), jsonDecoder.decoding(to: mapFailure)), + KeyValueTuple(.json(with: successStatusCodes), jsonDecoder.decoding(to: mapSuccess)), + KeyValueTuple(.json(with: failureStatusCodes), jsonDecoder.decoding(to: { + mapFailure(FailureMappingInput($0, rawResponse.statusCode)) + })), ]) let pluginResult: Result @@ -192,10 +194,10 @@ open class DefaultJsonNetworkService: ApiInteractor { preprocessors.append(securityPreprocessor) } - private static func preprocess(request: EndpointRequest, + private static func preprocess(request: EndpointRequest, preprocessors: P, cancellableBag: BaseCancellableBag, - completion: @escaping (Result, Error>) -> Void) + completion: @escaping (Result, Error>) -> Void) where P.Element == EndpointRequestPreprocessor { guard let preprocessor = preprocessors.first else { diff --git a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift index 453b3d0d..93a5e148 100644 --- a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift +++ b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift @@ -27,10 +27,11 @@ public protocol ApiInteractor { associatedtype NetworkError typealias RequestResult = EndpointRequestResult + typealias FailureMappingInput = (apiError: AE, statusCode: Int) func process(request: EndpointRequest, mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, + mapFailure: @escaping Closure, R>, mapNetworkError: @escaping Closure, completion: @escaping ParameterClosure) -> Cancellable } @@ -40,14 +41,14 @@ public extension ApiInteractor { func process(request: EndpointRequest) async -> RequestResult { await process(request: request, mapSuccess: Result.success, - mapFailure: { .failure(.apiError($0)) }, + mapFailure: { .failure(.apiError($0.apiError, $0.statusCode)) }, mapNetworkError: { .failure(.networkError($0)) }) } - func process(request: EndpointRequest, - mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, - mapNetworkError: @escaping Closure) async -> R { + func process(request: EndpointRequest, + mapSuccess: @escaping Closure, + mapFailure: @escaping Closure, R>, + mapNetworkError: @escaping Closure) async -> R { await withTaskCancellableClosure { completion in process(request: request, diff --git a/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift index 4e3f9b36..38e746de 100644 --- a/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift +++ b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift @@ -21,6 +21,6 @@ // public enum EndpointErrorResult: Error { - case apiError(ApiError) + case apiError(ApiError, Int) case networkError(NetworkError) } diff --git a/TINetworking/Sources/Response/ResponseType+Decoding.swift b/TINetworking/Sources/Response/ResponseType+Decoding.swift index ad2a0499..da78f1ac 100644 --- a/TINetworking/Sources/Response/ResponseType+Decoding.swift +++ b/TINetworking/Sources/Response/ResponseType+Decoding.swift @@ -23,15 +23,12 @@ import Foundation import TISwiftUtils -public typealias StatusCodeMimeType = (statusCode: Int, mimeType: String?) -public typealias StatusCodesMimeType = (statusCodes: Set, mimeType: String?) - -public typealias DecodingClosure = ThrowableClosure - public extension ResponseType { + typealias DecodingClosure = ThrowableClosure + func decode(mapping: [KeyValueTuple>]) -> Result { - for ((mappingStatusCode, mappingMimeType), decodeClosure) in mapping - where mappingStatusCode == statusCode && mappingMimeType == mimeType { + for (statusCodesMimeType, decodeClosure) in mapping + where statusCodesMimeType.statusCode == statusCode && statusCodesMimeType.mimeType == mimeType { do { return .success(try decodeClosure(data)) } catch { @@ -52,7 +49,8 @@ public extension ResponseType { func decode(mapping: [KeyValueTuple>]) -> Result { decode(mapping: mapping.map { key, value in - key.statusCodes.map { KeyValueTuple(StatusCodeMimeType($0, key.mimeType), value) } - }.flatMap { $0 }) + key.statusCodes.map { KeyValueTuple(StatusCodeMimeType(statusCode: $0, mimeType: key.mimeType), value) } + } + .flatMap { $0 }) } } diff --git a/TINetworking/Sources/Response/StatusCodeMimeType.swift b/TINetworking/Sources/Response/StatusCodeMimeType.swift new file mode 100644 index 00000000..6cfdc11d --- /dev/null +++ b/TINetworking/Sources/Response/StatusCodeMimeType.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct StatusCodeMimeType { + public let statusCode: Int + public let mimeType: String? + + public init(statusCode: Int, mimeType: String?) { + self.statusCode = statusCode + self.mimeType = mimeType + } + + public static func json(with statusCode: Int) -> Self { + .init(statusCode: statusCode, mimeType: CommonMediaTypes.applicationJson.rawValue) + } +} diff --git a/TINetworking/Sources/Response/StatusCodesMimeType.swift b/TINetworking/Sources/Response/StatusCodesMimeType.swift new file mode 100644 index 00000000..eae5fe1a --- /dev/null +++ b/TINetworking/Sources/Response/StatusCodesMimeType.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct StatusCodesMimeType { + public let statusCodes: Set + public let mimeType: String? + + public init(statusCodes: Set, mimeType: String?) { + self.statusCodes = statusCodes + self.mimeType = mimeType + } + + public static func json(with statusCodes: Set) -> Self { + .init(statusCodes: statusCodes, mimeType: CommonMediaTypes.applicationJson.rawValue) + } +} diff --git a/TIUIKitCore/Sources/Alerts/Models/AlertAction.swift b/TIUIKitCore/Sources/Alerts/Models/AlertAction.swift index d919c7ee..7b33cc55 100644 --- a/TIUIKitCore/Sources/Alerts/Models/AlertAction.swift +++ b/TIUIKitCore/Sources/Alerts/Models/AlertAction.swift @@ -29,13 +29,13 @@ public struct AlertAction { public let id = UUID() /// Alert button title - public let title: String + public var title: String /// Alert button style - public let style: UIAlertAction.Style + public var style: UIAlertAction.Style /// Alert button action - public let action: VoidClosure? + public var action: VoidClosure? public init(title: String, style: UIAlertAction.Style = .default, action: VoidClosure? = nil) { self.title = title diff --git a/TIUIKitCore/Sources/Alerts/Models/AlertDescriptor.swift b/TIUIKitCore/Sources/Alerts/Models/AlertDescriptor.swift index 2c6c81b8..2926b34c 100644 --- a/TIUIKitCore/Sources/Alerts/Models/AlertDescriptor.swift +++ b/TIUIKitCore/Sources/Alerts/Models/AlertDescriptor.swift @@ -26,19 +26,19 @@ import UIKit public struct AlertDescriptor { /// Alert title - public let title: String? + public var title: String? /// Alert message - public let message: String? + public var message: String? /// Alert style - public let style: UIAlertController.Style + public var style: UIAlertController.Style /// Alert tint color - public let tintColor: UIColor + public var tintColor: UIColor /// Alert actions - public let actions: [AlertAction] + public var actions: [AlertAction] public init(title: String? = nil, message: String? = nil,