feat: added HTTP status codes to `EndpointErrorResult.apiError` responses

This commit is contained in:
Ivan Smolin 2023-05-19 16:30:37 +03:00
parent dd4c9072a9
commit 6358386303
11 changed files with 119 additions and 36 deletions

View File

@ -1,5 +1,9 @@
# Changelog
### 1.44.0
- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses
### 1.43.1
- **Fixed**: build scripts submodule url

View File

@ -23,6 +23,8 @@
open class BaseCancellable: Cancellable {
private(set) public var isCancelled = false
public init() {}
open func cancel() {
isCancelled = true
}

View File

@ -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<T: AnyObject>(target: T?,
cancelClosure: @escaping WeakTargetCancellable<T>.CancelClosure) -> Cancellable {
WeakTargetCancellable(target: target, cancelClosure: cancelClosure)
}
}
@available(iOS 13.0.0, *)
public func withTaskCancellableClosure<T>(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()
})
}

View File

@ -68,9 +68,9 @@ open class DefaultJsonNetworkService: ApiInteractor {
defaultServer: openApi.defaultServer)
}
open func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
open func process<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
mapNetworkError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> TIFoundationUtils.Cancellable {
@ -115,9 +115,9 @@ open class DefaultJsonNetworkService: ApiInteractor {
return cancellableBag
}
open func process<S: Decodable, F: Decodable, R>(request: SerializedRequest,
open func process<S: Decodable, AE: Decodable, R>(request: SerializedRequest,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
mapNetworkError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> 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<Response, MoyaError>
@ -192,10 +194,10 @@ open class DefaultJsonNetworkService: ApiInteractor {
preprocessors.append(securityPreprocessor)
}
private static func preprocess<B,S,P: Collection>(request: EndpointRequest<B,S>,
private static func preprocess<B, S, P: Collection>(request: EndpointRequest<B, S>,
preprocessors: P,
cancellableBag: BaseCancellableBag,
completion: @escaping (Result<EndpointRequest<B,S>, Error>) -> Void)
completion: @escaping (Result<EndpointRequest<B, S>, Error>) -> Void)
where P.Element == EndpointRequestPreprocessor {
guard let preprocessor = preprocessors.first else {

View File

@ -27,10 +27,11 @@ public protocol ApiInteractor {
associatedtype NetworkError
typealias RequestResult<S: Decodable, AE: Decodable> = EndpointRequestResult<S, AE, NetworkError>
typealias FailureMappingInput<AE> = (apiError: AE, statusCode: Int)
func process<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<AE, R>,
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
mapNetworkError: @escaping Closure<NetworkError, R>,
completion: @escaping ParameterClosure<R>) -> Cancellable
}
@ -40,14 +41,14 @@ public extension ApiInteractor {
func process<B: Encodable, S, F>(request: EndpointRequest<B, S>) async -> RequestResult<S, F> {
await process(request: request,
mapSuccess: Result.success,
mapFailure: { .failure(.apiError($0)) },
mapFailure: { .failure(.apiError($0.apiError, $0.statusCode)) },
mapNetworkError: { .failure(.networkError($0)) })
}
func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapNetworkError: @escaping Closure<NetworkError, R>) async -> R {
func process<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
mapNetworkError: @escaping Closure<NetworkError, R>) async -> R {
await withTaskCancellableClosure { completion in
process(request: request,

View File

@ -21,6 +21,6 @@
//
public enum EndpointErrorResult<ApiError, NetworkError>: Error {
case apiError(ApiError)
case apiError(ApiError, Int)
case networkError(NetworkError)
}

View File

@ -23,15 +23,12 @@
import Foundation
import TISwiftUtils
public typealias StatusCodeMimeType = (statusCode: Int, mimeType: String?)
public typealias StatusCodesMimeType = (statusCodes: Set<Int>, mimeType: String?)
public typealias DecodingClosure<R> = ThrowableClosure<Data, R>
public extension ResponseType {
typealias DecodingClosure<R> = ThrowableClosure<Data, R>
func decode<R>(mapping: [KeyValueTuple<StatusCodeMimeType, DecodingClosure<R>>]) -> Result<R, ErrorType> {
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<R>(mapping: [KeyValueTuple<StatusCodesMimeType, DecodingClosure<R>>]) -> Result<R, ErrorType> {
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 })
}
}

View File

@ -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)
}
}

View File

@ -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<Int>
public let mimeType: String?
public init(statusCodes: Set<Int>, mimeType: String?) {
self.statusCodes = statusCodes
self.mimeType = mimeType
}
public static func json(with statusCodes: Set<Int>) -> Self {
.init(statusCodes: statusCodes, mimeType: CommonMediaTypes.applicationJson.rawValue)
}
}

View File

@ -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

View File

@ -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,