feat: Change access modifiers in `DefaultJsonNetworkService` from `public` to `open`, added additional Moya plugins processing

add `DisplayDecodingErrorPlugin` for showing developer-frendly decoding error messages
add Gemfile for cocoapods versioning
This commit is contained in:
Ivan Smolin 2022-04-06 13:44:06 +03:00
parent e67136013c
commit 10ec9408ad
19 changed files with 370 additions and 29 deletions

2
.bundle/config Normal file
View File

@ -0,0 +1,2 @@
---
BUNDLE_PATH: ".gem"

3
.gitignore vendored
View File

@ -104,4 +104,7 @@ cpd-output.xml
*.swp
*IDEWorkspaceChecks.plist
# Gem
.gem/
.DS_Store

View File

@ -1,5 +1,11 @@
# Changelog
### 1.13.0
- **Update**: Change access modifiers in `DefaultJsonNetworkService` from `public` to `open`, added additional Moya plugins processing
- **Add**: `DisplayDecodingErrorPlugin` for showing developer-frendly decoding error messages
- **Add**: Gemfile for cocoapods versioning
### 1.12.3
- **Fix**: Try parse date in ISO8601 format appending `.withFractionalSeconds` if `.withInternetDateTime` fails

5
Gemfile Normal file
View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "cocoapods", "~> 1.11"

97
Gemfile.lock Normal file
View File

@ -0,0 +1,97 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.2)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.10)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.6.1)
minitest (5.15.0)
molinillo (0.8.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.6)
rexml (3.2.5)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.5.4)
PLATFORMS
x86_64-darwin-20
DEPENDENCIES
cocoapods (~> 1.11)
BUNDLED WITH
2.3.10

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.12.3"
s.version = "1.13.0"
s.summary = "iOS framework with a bunch of tools for rapid development"
s.homepage = "https://github.com/TouchInstinct/LeadKit"
s.license = "Apache License, Version 2.0"

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -60,8 +60,8 @@ open class DefaultJsonNetworkService {
}
@available(iOS 13.0.0, *)
public func process<B: Encodable, S: Decodable, F: Decodable>(request: EndpointRequest<B, S>,
mapMoyaError: @escaping Closure<MoyaError, F>) async -> Result<S, F> {
open func process<B: Encodable, S: Decodable, F: Decodable>(request: EndpointRequest<B, S>,
mapMoyaError: @escaping Closure<MoyaError, F>) async -> Result<S, F> {
await process(request: request,
mapSuccess: Result.success,
mapFailure: Result.failure,
@ -69,10 +69,10 @@ open class DefaultJsonNetworkService {
}
@available(iOS 13.0.0, *)
public func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>) async -> R {
open func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>) async -> R {
let cancellableBag = CancellableBag()
@ -87,16 +87,16 @@ open class DefaultJsonNetworkService {
continuation.resume(returning: $0)
}
.add(to: cancellableBag)
.add(to: cancellableBag)
}
})
}
public func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> Cancellable {
open func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> Cancellable {
ScopeCancellable { [jsonEncoder, serializationQueue, callbackQueue, defaultServer] scope in
let workItem = DispatchWorkItem {
@ -126,13 +126,17 @@ open class DefaultJsonNetworkService {
}
}
public func process<S: Decodable, F: Decodable, R>(request: SerializedRequest,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> Cancellable {
open func process<S: Decodable, F: Decodable, R>(request: SerializedRequest,
mapSuccess: @escaping Closure<S, R>,
mapFailure: @escaping Closure<F, R>,
mapMoyaError: @escaping Closure<MoyaError, R>,
completion: @escaping ParameterClosure<R>) -> Cancellable {
createProvider().request(request) { [jsonDecoder, callbackQueue, decodableSuccessStatusCodes, decodableFailureStatusCodes] in
createProvider().request(request) { [jsonDecoder,
callbackQueue,
decodableSuccessStatusCodes,
decodableFailureStatusCodes,
plugins] in
let result: R
switch $0 {
@ -163,11 +167,19 @@ open class DefaultJsonNetworkService {
((failureStatusCodes, CommonMediaTypes.applicationJson.rawValue), jsonDecoder.decoding(to: mapFailure)),
])
let pluginResult: Result<Response, MoyaError>
switch decodeResult {
case let .success(model):
result = model
pluginResult = .success(rawResponse)
case let .failure(moyaError):
result = mapMoyaError(moyaError)
pluginResult = .failure(moyaError)
}
plugins.forEach {
$0.didReceive(pluginResult, target: request)
}
case let .failure(moyaError):
result = mapMoyaError(moyaError)

View File

@ -0,0 +1,108 @@
//
// Copyright (c) 2022 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.
//
import Moya
import Foundation
public struct DecodingErrorSummary: CustomStringConvertible {
public let url: String
public let requestMethod: String
public let statusCode: Int
public let codingPath: String
public let jsonValue: String
public let errorDescription: String
public let formatter: DecodingErrorSummaryFormatter
public var description: String {
formatter.format(summary: self)
}
public init(decodingError: DecodingError,
response: Response,
formatter: DecodingErrorSummaryFormatter) {
switch decodingError {
case let .typeMismatch(_, context):
self.init(context: context, response: response, formatter: formatter)
case let .dataCorrupted(context):
self.init(context: context, response: response, formatter: formatter)
case let .keyNotFound(_, context):
self.init(context: context, response: response, formatter: formatter)
case let .valueNotFound(_, context):
self.init(context: context, response: response, formatter: formatter)
@unknown default:
self.init(context: .init(codingPath: [],
debugDescription: ">>UNHANDLED DECODING ERROR CASE<<",
underlyingError: nil),
response: response,
formatter: formatter)
}
}
public init(context: DecodingError.Context,
response: Response,
formatter: DecodingErrorSummaryFormatter) {
self.url = response.request?.url?.relativePath ?? formatter.missingDataPlaceholder
self.statusCode = response.statusCode
self.requestMethod = response.request?.httpMethod ?? formatter.missingDataPlaceholder
self.codingPath = formatter.format(codingPath: context.codingPath)
self.errorDescription = context.debugDescription
self.jsonValue = formatter.format(jsonValue: context.extractJsonValue(from: response))
self.formatter = formatter
}
}
private extension DecodingError.Context {
func extractJsonValue(from response: Response) -> Any? {
do {
let jsonObject = try JSONSerialization.jsonObject(with: response.data, options: .fragmentsAllowed)
return value(forCodingPath: codingPath, in: jsonObject)
} catch {
return nil
}
}
}
private func value<C: Collection>(forCodingPath path: C, in obj: Any?) -> Any? where C.Element == CodingKey {
guard let part = path.first else {
return obj
}
switch obj {
case let dict as [AnyHashable: Any]:
return value(forCodingPath: path.dropFirst(), in: dict[part.stringValue])
case let array as [Any]:
guard let arrayIndex = part.intValue else {
return nil
}
return value(forCodingPath: path.dropFirst(), in: array[arrayIndex])
case let anyObj as AnyObject:
return value(forCodingPath: path.dropFirst(), in: anyObj.value(forKey: part.stringValue))
default:
return nil
}
}

View File

@ -0,0 +1,61 @@
//
// Copyright (c) 2022 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.
//
open class DecodingErrorSummaryFormatter {
public var codingPathSeparator = "."
public var missingDataPlaceholder = "N/A"
public init() {}
open func format(summary: DecodingErrorSummary) -> String {
"""
\(summary.errorDescription)
json_value: \(summary.jsonValue)
json_path: \(summary.codingPath)
\(summary.requestMethod) \(summary.statusCode) \(summary.url)
"""
}
open func format(codingPath: [CodingKey]) -> String {
let formattedPath = codingPath.reduce(into: "") {
if let intValue = $1.intValue {
$0.append("[\(intValue)]")
} else {
$0.append(codingPathSeparator)
$0.append($1.stringValue)
}
}
guard formattedPath.range(of: codingPathSeparator)?.lowerBound != formattedPath.startIndex else {
return String(formattedPath.dropFirst())
}
return formattedPath
}
open func format(jsonValue: Any?) -> String {
String(reflecting: jsonValue ?? missingDataPlaceholder)
}
}

View File

@ -0,0 +1,47 @@
//
// Copyright (c) 2022 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.
//
import Moya
open class DisplayDecodingErrorPlugin: PluginType {
public var formatter: DecodingErrorSummaryFormatter
public init(formatter: DecodingErrorSummaryFormatter = .init()) {
self.formatter = formatter
}
open func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
guard case let .failure(moyaError) = result else {
return
}
guard case let .objectMapping(error as DecodingError, response) = moyaError else {
return
}
display(summary: .init(decodingError: error, response: response, formatter: formatter))
}
open func display(summary: DecodingErrorSummary) {
debugPrint(summary.description)
}
}

View File

@ -1,13 +1,13 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.ios.deployment_target = '11.0'
s.swift_versions = ['5.3']
s.source_files = s.name + '/**/Sources/**/*'

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITransitions'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Set of custom transitions to present controller. '
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.12.3'
s.version = '1.13.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }