Last active
April 29, 2019 07:43
-
-
Save dehrom/c8dc80bba531889ecc33e3071e79f0dd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
private let errorDebugDescriptionCreateBlock: (String, ContentRepresentable & IntCodeRepresentable & LocalizedErrorRepresentable) -> String = { name, error in | |
return """ | |
=== Self is \(name) === | |
Code: \(error.code) \r | |
errorDescription: \(String(describing: error.asLocalizedError?.errorDescription)) \r | |
failureReason: \(String(describing: error.asLocalizedError?.failureReason)) \r | |
recoverySuggestion: \(String(describing: error.asLocalizedError?.recoverySuggestion)) \r | |
helpAnchor: \(String(describing: error.asLocalizedError?.helpAnchor)) \r | |
content: \(error.content.debugDescription) \r\n | |
""" | |
} | |
public protocol ContentRepresentable { | |
var content: [String: Any] { get } | |
} | |
public extension ContentRepresentable { | |
var content: [String: Any] { | |
return [:] | |
} | |
} | |
public protocol IntCodeRepresentable { | |
var code: Int { get } | |
} | |
public extension IntCodeRepresentable where Self: RawRepresentable, Self.RawValue == Int { | |
var code: Int { | |
return rawValue | |
} | |
} | |
public protocol LocalizedErrorRepresentable { | |
var asLocalizedError: LocalizedError? { get } | |
} | |
public extension LocalizedErrorRepresentable where Self: LocalizedError { | |
var asLocalizedError: LocalizedError? { | |
return self | |
} | |
} | |
public protocol ChainableErrorRepresentable: IntCodeRepresentable, LocalizedErrorRepresentable, ContentRepresentable, CustomDebugStringConvertible {} | |
public extension ChainableErrorRepresentable where Self: LocalizedError { | |
var debugDescription: String { | |
return errorDebugDescriptionCreateBlock("\(type(of: self))", self) | |
} | |
} | |
public protocol ChainableError: RawRepresentable, LocalizedError, ChainableErrorRepresentable where Self.RawValue == Int {} | |
extension ChainableError { | |
public func chained(_ error: Error, content: [String: Any] = [:]) -> Error { | |
switch error { | |
case let (chain as Chainable): | |
return chained(withChainable: chain, content: content) | |
case let (baseError as ChainableErrorRepresentable): | |
return chained(baseError, content: content) | |
default: | |
preconditionFailure("Unchainable error: \(error) of type \(type(of: error)). Please handle error previously!") | |
} | |
} | |
} | |
private extension ChainableError { | |
private func chained(withChainable error: Chainable, content: [String: Any]) -> Error { | |
var head = ChainErrorBox(from: self, content: content) | |
head.chain(with: error) | |
return head | |
} | |
private func chained(_ error: ChainableErrorRepresentable, content: [String: Any]) -> Error { | |
let box = ChainErrorBox(from: error, content: error.content) | |
return chained(withChainable: box, content: content) | |
} | |
} | |
internal protocol Chainable { | |
var previous: Chainable? { get } | |
mutating func chain(with previous: Chainable) | |
} | |
internal protocol ChainIntCodesCollectable: Chainable, ChainableErrorRepresentable {} | |
extension ChainIntCodesCollectable { | |
var chainedCodes: [Int] { | |
func collectFrom(_ node: Self?) -> [Int] { | |
guard let node = node else { return [] } | |
return [node.code] + collectFrom(node.previous as? Self) | |
} | |
return collectFrom(self) | |
} | |
} | |
private struct ChainErrorBox: ChainIntCodesCollectable, Error { | |
init(from error: ChainableErrorRepresentable, content: [String : Any]) { | |
boxed = error | |
self.content = content | |
} | |
var code: Int { | |
return boxed.code | |
} | |
var asLocalizedError: LocalizedError? { | |
return boxed.asLocalizedError | |
} | |
var debugDescription: String { | |
return errorDebugDescriptionCreateBlock("\(type(of: boxed))", self) | |
} | |
mutating func chain(with previous: Chainable) { | |
self.previous = previous | |
} | |
internal let content: [String : Any] | |
private let boxed: ChainableErrorRepresentable | |
private(set) var previous: Chainable? | |
} | |
extension ChainErrorBox: LocalizedError { | |
var errorDescription: String? { | |
return boxed.asLocalizedError?.errorDescription | |
} | |
var failureReason: String? { | |
return boxed.asLocalizedError?.failureReason | |
} | |
var recoverySuggestion: String? { | |
return boxed.asLocalizedError?.recoverySuggestion | |
} | |
var helpAnchor: String? { | |
return boxed.asLocalizedError?.helpAnchor | |
} | |
} | |
public struct ChainableErrorFormatter { | |
public let identificator: String | |
public init(identificator: String) { | |
self.identificator = identificator | |
} | |
public func string(for error: Error, defaultValue: String? = nil) -> String { | |
switch (error as? ChainErrorBox, defaultValue) { | |
case let (.some(error), _): | |
return "\(identificator)::\(error.chainedCodes.map { "\($0)" }.joined(separator: "-"))" | |
case (.none, .none): | |
assertionFailure("This error \(error) with type \(type(of: error.self)) could not be presented as chain!") | |
return "ILLEGAL ERROR TYPE" | |
case let (.none, .some(message)): | |
return message | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum ApiError: Int, Error { | |
case error0 = 4 | |
case error1 = 6 | |
} | |
extension ApiError: ChainableErrorRepresentable, LocalizedError { | |
static let atemptsCount = "atemptsCount" | |
var content: [String : Any] { | |
switch self { | |
case .error0: | |
return [ApiError.atemptsCount: code] | |
case .error1: | |
return [ApiError.atemptsCount: code] | |
} | |
} | |
} | |
enum ServiceError: Int, ChainableError { | |
case serverIsBusy = 7 | |
case apiInternalError = 11 | |
case `internal` | |
} | |
enum InteractorError: Int, ChainableError { | |
case `internal` | |
case logic = 4 | |
} | |
final class TestErrors { | |
func test() { | |
// service | |
let apiResponse = Result<Data, ApiError>.failure(.error0) | |
let serviceError = { () -> Error in | |
switch (apiResponse) { | |
case let .failure(error) where error == ApiError.error0: | |
return ServiceError.apiInternalError.chained(error, content: error.content) | |
case let .failure(error) where error == ApiError.error1: | |
return ServiceError.serverIsBusy.chained(error, content: error.content) | |
default: | |
return ServiceError.internal | |
} | |
}() | |
/* | |
=== Self is ServiceError === | |
Code: 11 | |
errorDescription: nil | |
failureReason: nil | |
recoverySuggestion: nil | |
helpAnchor: nil | |
content: ["atemptsCount": 4] | |
*/ | |
// interactor | |
let interactorError = { () -> Error in | |
switch (serviceError as? ChainableErrorRepresentable) { | |
case let (.some(error)) where error.code == ServiceError.apiInternalError.rawValue: | |
return InteractorError.logic.chained(serviceError, content: error.content) | |
default: | |
return InteractorError.internal.chained(serviceError) | |
} | |
}() | |
/* | |
=== Self is InteractorError === | |
Code: 4 | |
errorDescription: nil | |
failureReason: nil | |
recoverySuggestion: nil | |
helpAnchor: nil | |
content: ["atemptsCount": 4] | |
*/ | |
let str = ChainableErrorFormatter(identificator: "TEST").string(for: interactorError) | |
print(str) // "TEST::4-11-4" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment