diff --git a/Package.swift b/Package.swift index c7bed74..1fa6603 100644 --- a/Package.swift +++ b/Package.swift @@ -1,14 +1,14 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "RetroSwift", platforms: [ - .macOS("10.15"), - .iOS("13.0"), - .tvOS("13.0"), - .watchOS("6.0") + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6) ], products: [ .library( @@ -21,7 +21,6 @@ let package = Package( path: "RetroSwift"), .testTarget( name: "RetroSwiftTests", - // FIXME: Linter to be added dependencies: ["RetroSwift"], path: "RetroSwiftTests") ] diff --git a/RetroSwift/ApiContractDescription/Domain/Domain.swift b/RetroSwift/ApiContractDescription/Domain/Domain.swift index f72af5f..e7922cc 100644 --- a/RetroSwift/ApiContractDescription/Domain/Domain.swift +++ b/RetroSwift/ApiContractDescription/Domain/Domain.swift @@ -36,11 +36,61 @@ open class Domain { let operationResult = try await transport.sendRequest(with: requestParams) let responseData = try operationResult.response.get() - if responseData.isEmpty, Response.self is Empty.Type { - // swiftlint:disable:next force_cast - return Empty() as! Response - } else { + if responseData.isEmpty { + if Response.self is EmptyResponseDecodable.Type || Domain.isEitherWithEmptyResponse(Response.self) { + return try JSONDecoder().decode(Response.self, from: Domain.emptyJsonData) + } + } + + do { return try JSONDecoder().decode(Response.self, from: responseData) + } catch let decodingError as DecodingError { + let errorDescription = Domain.describeDecodingError(decodingError, data: responseData) + print("[RetroSwift] Decoding error: \(errorDescription)") + throw decodingError } } } + +private extension Domain { + static func describeDecodingError(_ error: DecodingError, data: Data) -> String { + let jsonPreview = String(data: data.prefix(500), encoding: .utf8) ?? "Unable to preview" + + switch error { + case .keyNotFound(let key, let context): + return """ + Key '\(key.stringValue)' not found. + Path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> ")) + Debug: \(context.debugDescription) + JSON preview: \(jsonPreview) + """ + case .typeMismatch(let type, let context): + return """ + Type mismatch for type '\(type)'. + Path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> ")) + Debug: \(context.debugDescription) + JSON preview: \(jsonPreview) + """ + case .valueNotFound(let type, let context): + return """ + Value of type '\(type)' not found. + Path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> ")) + Debug: \(context.debugDescription) + JSON preview: \(jsonPreview) + """ + case .dataCorrupted(let context): + return """ + Data corrupted. + Path: \(context.codingPath.map { $0.stringValue }.joined(separator: " -> ")) + Debug: \(context.debugDescription) + JSON preview: \(jsonPreview) + """ + @unknown default: + return "Unknown decoding error: \(error.localizedDescription)" + } + } +} + +private extension Domain { + static let emptyJsonData = Data("{}".utf8) +} diff --git a/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Either.swift b/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Either.swift index ff36732..586798a 100644 --- a/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Either.swift +++ b/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Either.swift @@ -1,21 +1,62 @@ import Foundation -extension Domain { - public enum Either: Decodable { +public extension Domain { + enum Either: Decodable { case response(Response) case errorResponse(ErrorResponse) } } -extension Domain.Either { - public init(from decoder: Decoder) throws { +private protocol EitherCheckable { + static var responseType: Any.Type { get } +} + +extension Domain.Either: EitherCheckable { + static var responseType: Any.Type { Response.self } +} + +extension Domain { + static func isEitherWithEmptyResponse(_ type: Any.Type) -> Bool { + guard let eitherType = type as? EitherCheckable.Type else { + return false + } + return eitherType.responseType is EmptyResponseDecodable.Type + } +} + +public extension Domain.Either { + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() + if Response.self is EmptyResponseDecodable.Type { + if let errorValue = try? container.decode(ErrorResponse.self), + !errorValue.isEmptyErrorResponse { + self = .errorResponse(errorValue) + return + } + self = try .response(container.decode(Response.self)) + return + } + if let value = try? container.decode(Response.self) { self = .response(value) - } else { - let errorValue = try container.decode(ErrorResponse.self) - self = .errorResponse(errorValue) + return } + let errorValue = try container.decode(ErrorResponse.self) + self = .errorResponse(errorValue) + } +} + +public protocol EmptyErrorCheckable { + var isEmptyErrorResponse: Bool { get } +} + +extension EmptyErrorCheckable { + public var isEmptyErrorResponse: Bool { false } +} + +extension Decodable { + var isEmptyErrorResponse: Bool { + (self as? EmptyErrorCheckable)?.isEmptyErrorResponse ?? false } } diff --git a/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Empty.swift b/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Empty.swift index 40d9483..9f12acc 100644 --- a/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Empty.swift +++ b/RetroSwift/ApiContractDescription/Domain/ResponseTypes/Domain.Empty.swift @@ -1,5 +1,11 @@ import Foundation +public protocol EmptyResponseDecodable: Decodable { + init() +} + extension Domain { - public struct Empty: Decodable { } + public struct Empty: EmptyResponseDecodable { + public init() {} + } }