Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweaks to DecodingFailureInitializable #71

Merged
merged 11 commits into from
Oct 5, 2018
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Remove the type definitions deprecated in 2.0.0
[Will McGinty](https://github.com/wmcginty)
[#72](https://github.com/BottleRocketStudios/iOS-Hyperspace/pull/72)
* Added failing type information to `DecodingFailureInitializable` allowing the API to make decisions based off of the type that failed to decode and deprecate dynamically keyed decoding.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but can we get a newline before this entry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Stupid merge.

[Will McGinty](https://github.com/wmcginty)
[#71](https://github.com/BottleRocketStudios/iOS-Hyperspace/pull/71)

##### Bug Fixes

Expand Down
20 changes: 20 additions & 0 deletions Hyperspace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
objects = {

/* Begin PBXBuildFile section */
0E29092D21349F600031F67C /* DecodingFailureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E29092B21349F320031F67C /* DecodingFailureTests.swift */; };
0E29092E21349F610031F67C /* DecodingFailureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E29092B21349F320031F67C /* DecodingFailureTests.swift */; };
0E347E5B2155A7A500BF18FA /* AnyError+Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E347E5A2155A7A500BF18FA /* AnyError+Request.swift */; };
0E347E5C2155AC3700BF18FA /* AnyError+Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E347E5A2155A7A500BF18FA /* AnyError+Request.swift */; };
0E347E5D2155AC3700BF18FA /* AnyError+Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E347E5A2155A7A500BF18FA /* AnyError+Request.swift */; };
0E347E6421599B7D00BF18FA /* XCTestCase+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E347E6221599B5900BF18FA /* XCTestCase+JSON.swift */; };
0E347E6521599B7F00BF18FA /* XCTestCase+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E347E6221599B5900BF18FA /* XCTestCase+JSON.swift */; };
0E81B49920AC8E0B00DC1F4E /* RecoveryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E81B49820AC8E0B00DC1F4E /* RecoveryStrategy.swift */; };
0E81B49B20AC97CA00DC1F4E /* RecoverableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E81B49A20AC97C900DC1F4E /* RecoverableTests.swift */; };
0E81B49C20AC97CA00DC1F4E /* RecoverableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E81B49A20AC97C900DC1F4E /* RecoverableTests.swift */; };
Expand Down Expand Up @@ -282,6 +289,9 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0E29092B21349F320031F67C /* DecodingFailureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingFailureTests.swift; sourceTree = "<group>"; };
0E347E5A2155A7A500BF18FA /* AnyError+Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyError+Request.swift"; sourceTree = "<group>"; };
0E347E6221599B5900BF18FA /* XCTestCase+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+JSON.swift"; sourceTree = "<group>"; };
0E81B49820AC8E0B00DC1F4E /* RecoveryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryStrategy.swift; sourceTree = "<group>"; };
0E81B49A20AC97C900DC1F4E /* RecoverableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoverableTests.swift; sourceTree = "<group>"; };
0E81B4A120ADE0E100DC1F4E /* JSONDecoder+DecodableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+DecodableContainer.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -549,6 +559,7 @@
60D6C3F0201CFCCF00B3B012 /* BackendServiceTests.swift */,
0E81B49A20AC97C900DC1F4E /* RecoverableTests.swift */,
60D6C3F1201CFCCF00B3B012 /* DecodingTests.swift */,
0E29092B21349F320031F67C /* DecodingFailureTests.swift */,
60D6C3F7201CFCCF00B3B012 /* NetworkRequestTests.swift */,
60D6C3EF201CFCCF00B3B012 /* NetworkServiceTests.swift */,
3026E457202008470003A321 /* NetworkSessionTest.swift */,
Expand Down Expand Up @@ -578,6 +589,7 @@
60AC833F1FF596B600120172 /* URL+Additions.swift */,
60AC83401FF596B600120172 /* URLQueryItem+Extensions.swift */,
0E81B4A120ADE0E100DC1F4E /* JSONDecoder+DecodableContainer.swift */,
0E347E5A2155A7A500BF18FA /* AnyError+Request.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -686,6 +698,7 @@
60D6C3FD201CFCCF00B3B012 /* Test Defaults */,
60D6C3F2201CFCCF00B3B012 /* Mocks */,
60D6C3F8201CFCCF00B3B012 /* JSON */,
0E347E6221599B5900BF18FA /* XCTestCase+JSON.swift */,
);
path = Helper;
sourceTree = "<group>";
Expand Down Expand Up @@ -1180,6 +1193,7 @@
60D6C370201CC81100B3B012 /* DecodableContainer.swift in Sources */,
60127D5C20686AF8002B7BB9 /* NetworkServiceHelper.swift in Sources */,
60D6C371201CC81100B3B012 /* AnyRequest.swift in Sources */,
0E347E5B2155A7A500BF18FA /* AnyError+Request.swift in Sources */,
60D6C372201CC81100B3B012 /* Request.swift in Sources */,
60D6C373201CC81100B3B012 /* NetworkSessionDataTask.swift in Sources */,
0ECDC1CF20A9FE3100ABF991 /* AnyDecodable.swift in Sources */,
Expand All @@ -1203,6 +1217,7 @@
60D6C409201CFCCF00B3B012 /* MockNetworkService.swift in Sources */,
60D6C403201CFCCF00B3B012 /* DecodingTests.swift in Sources */,
0E81B49B20AC97CA00DC1F4E /* RecoverableTests.swift in Sources */,
0E347E6521599B7F00BF18FA /* XCTestCase+JSON.swift in Sources */,
30F07361202213E50045CB01 /* NetworkRequestTestDefaults.swift in Sources */,
0ECDC1C420A9EDA500ABF991 /* URLQueryParameterTests.swift in Sources */,
60D6C40D201CFCCF00B3B012 /* NetworkRequestTests.swift in Sources */,
Expand All @@ -1214,6 +1229,7 @@
30F07358202172420045CB01 /* MockBackendService.swift in Sources */,
B4C32C30201E9F8400FC82C1 /* MockNetworkActivityIndicator.swift in Sources */,
60D6C3FF201CFCCF00B3B012 /* NetworkServiceTests.swift in Sources */,
0E29092D21349F600031F67C /* DecodingFailureTests.swift in Sources */,
60D6C405201CFCCF00B3B012 /* MockNetworkSession.swift in Sources */,
30F07367202227790045CB01 /* NetworkServiceMockSubclass.swift in Sources */,
);
Expand All @@ -1230,6 +1246,7 @@
0ECDC1DE20AB3E6C00ABF991 /* URLQueryParameterEncoder.swift in Sources */,
60D6C388201CCCBE00B3B012 /* DecodableContainer.swift in Sources */,
60127D5E20686AF8002B7BB9 /* NetworkServiceHelper.swift in Sources */,
0E347E5D2155AC3700BF18FA /* AnyError+Request.swift in Sources */,
60D6C389201CCCBE00B3B012 /* AnyRequest.swift in Sources */,
60D6C38A201CCCBE00B3B012 /* Request.swift in Sources */,
60D6C38B201CCCBE00B3B012 /* NetworkSessionDataTask.swift in Sources */,
Expand All @@ -1256,6 +1273,7 @@
0ECDC1DD20AB3E6B00ABF991 /* URLQueryParameterEncoder.swift in Sources */,
60D6C37D201CCCAE00B3B012 /* AnyRequest.swift in Sources */,
60127D5D20686AF8002B7BB9 /* NetworkServiceHelper.swift in Sources */,
0E347E5C2155AC3700BF18FA /* AnyError+Request.swift in Sources */,
60D6C37E201CCCAE00B3B012 /* Request.swift in Sources */,
60D6C37F201CCCAE00B3B012 /* NetworkSessionDataTask.swift in Sources */,
60D6C380201CCCAE00B3B012 /* NetworkServiceProtocol.swift in Sources */,
Expand All @@ -1279,6 +1297,7 @@
60D6C40A201CFCCF00B3B012 /* MockNetworkService.swift in Sources */,
60D6C404201CFCCF00B3B012 /* DecodingTests.swift in Sources */,
0E81B49C20AC97CA00DC1F4E /* RecoverableTests.swift in Sources */,
0E347E6421599B7D00BF18FA /* XCTestCase+JSON.swift in Sources */,
30F07362202213E60045CB01 /* NetworkRequestTestDefaults.swift in Sources */,
0ECDC1C520A9EDA600ABF991 /* URLQueryParameterTests.swift in Sources */,
60D6C40E201CFCCF00B3B012 /* NetworkRequestTests.swift in Sources */,
Expand All @@ -1290,6 +1309,7 @@
30F07359202172450045CB01 /* MockBackendService.swift in Sources */,
B4C32C31201E9F8400FC82C1 /* MockNetworkActivityIndicator.swift in Sources */,
60D6C400201CFCCF00B3B012 /* NetworkServiceTests.swift in Sources */,
0E29092E21349F610031F67C /* DecodingFailureTests.swift in Sources */,
60D6C406201CFCCF00B3B012 /* MockNetworkSession.swift in Sources */,
30F073682022277A0045CB01 /* NetworkServiceMockSubclass.swift in Sources */,
);
Expand Down
35 changes: 35 additions & 0 deletions Sources/Hyperspace/Extensions/AnyError+Request.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// AnyError+Request.swift
// Hyperspace-iOS
//
// Created by Will McGinty on 9/21/18.
// Copyright © 2018 Bottle Rocket Studios. All rights reserved.
//

import Foundation
import Result

// MARK: - AnyError Conformance to NetworkServiceInitializable

extension AnyError: NetworkServiceFailureInitializable {

public init(networkServiceFailure: NetworkServiceFailure) {
self.init(networkServiceFailure.error)
}

public var networkServiceError: NetworkServiceError {
return (error as? NetworkServiceError) ?? .unknownError
}

public var failureResponse: HTTP.Response? {
return nil
}
}

// MARK: - AnyError Conformance to DecodingFailureInitializable

extension AnyError: DecodingFailureInitializable {
public init(error: DecodingError, decoding: Decodable.Type, data: Data) {
self.init(error)
}
}
1 change: 0 additions & 1 deletion Sources/Hyperspace/HTTP/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

//
// TODO: Future functionality:
// - Consider converting HTTP.Status to a "RawRepresentable" type with static constants for codes. This would allow clients to extend or provide their own status codes and eliminate the need for an 'unknown' enum case.
// - Are there any HTTP.HeaderKeys that we should add (or remove)?
// - Are there any HTTP.HeaderValues that we should add (or remove)?
//
Expand Down
9 changes: 2 additions & 7 deletions Sources/Hyperspace/Request/Decoding/DecodableContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,10 @@ extension AnyRequest where T: Decodable {
timeout: TimeInterval = RequestDefaults.defaultTimeout,
decoder: JSONDecoder = JSONDecoder(),
containerType: U.Type) where U.ContainedType == T {
self.init(method: method, url: url, headers: headers, body: body, cachePolicy: cachePolicy, timeout: timeout) { data in
do {
return try .success(decoder.decode(U.ContainedType.self, from: data, with: U.self))
} catch {
return .failure(AnyError(error))
}
}
self.init(method: method, url: url, headers: headers, body: body, cachePolicy: cachePolicy, timeout: timeout, dataTransformer: RequestDefaults.dataTransformer(for: decoder, withContainerType: containerType))
}

@available(*, deprecated, message: "This method of dynamically assigning a rootKey is not only unsafe but non-performant. Users of this API should migrate to `DecodableContainer` usage instead.")
public init(method: HTTP.Method,
url: URL,
headers: [HTTP.HeaderKey: HTTP.HeaderValue]? = nil,
Expand Down
52 changes: 16 additions & 36 deletions Sources/Hyperspace/Request/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public protocol NetworkServiceFailureInitializable: Swift.Error {

/// Represents an error which can be constructed from a `DecodingError` and `Data`.
public protocol DecodingFailureInitializable: Swift.Error {
init(decodingError: DecodingError, data: Data)
init(error: DecodingError, decoding: Decodable.Type, data: Data)
}

/// Encapsulates all the necessary parameters to represent a request that can be sent over the network.
Expand Down Expand Up @@ -82,46 +82,49 @@ public struct RequestDefaults {

public static var defaultTimeout: TimeInterval = 30

public typealias CatchErrorTransformer<E> = (Swift.Error, Data) -> E
public typealias DecodingErrorTransformer<E> = (Swift.Error, Any.Type, Data) -> E

public static func dataTransformer<ResponseType: Decodable, ErrorType>(for decoder: JSONDecoder, catchTransformer: @escaping CatchErrorTransformer<ErrorType>) -> (Data) -> Result<ResponseType, ErrorType> {
public static func dataTransformer<ResponseType: Decodable, ErrorType>(for decoder: JSONDecoder, catchTransformer: @escaping DecodingErrorTransformer<ErrorType>) -> (Data) -> Result<ResponseType, ErrorType> {
return { data in
do {
let decodedResponse: ResponseType = try decoder.decode(ResponseType.self, from: data)
return .success(decodedResponse)
} catch {
return .failure(catchTransformer(error, data))
return .failure(catchTransformer(error, ResponseType.self, data))
}
}
}

public static func dataTransformer<ResponseType: Decodable, ErrorType: DecodingFailureInitializable>(for decoder: JSONDecoder) -> (Data) -> Result<ResponseType, ErrorType> {
return dataTransformer(for: decoder) {
guard let decodingError = $0 as? DecodingError else { fatalError("JSONDecoder should always throw a DecodingError.") }
return ErrorType(decodingError: decodingError, data: $1)
return error(from: $0, decoding: ResponseType.self, from: $2)
}
}

public static func dataTransformer<ContainerType: DecodableContainer, ErrorType>(for decoder: JSONDecoder, withContainerType containerType: ContainerType.Type,
catchTransformer: @escaping CatchErrorTransformer<ErrorType>) -> (Data) -> Result<ContainerType.ContainedType, ErrorType> {
catchTransformer: @escaping DecodingErrorTransformer<ErrorType>) -> (Data) -> Result<ContainerType.ContainedType, ErrorType> {
return { data in
do {

let decodedResponse: ContainerType.ContainedType = try decoder.decode(ContainerType.ContainedType.self, from: data, with: containerType)
return .success(decodedResponse)
} catch {
return .failure(catchTransformer(error, data))
return .failure(catchTransformer(error, ContainerType.ContainedType.self, data))
}
}
}

public static func dataTransformer<ContainerType: DecodableContainer, ErrorType: DecodingFailureInitializable>(for decoder: JSONDecoder,
withContainerType containerType: ContainerType.Type) -> (Data) -> Result<ContainerType.ContainedType, ErrorType> {
return dataTransformer(for: decoder, withContainerType: containerType) {
guard let decodingError = $0 as? DecodingError else { fatalError("JSONDecoder should always throw a DecodingError.") }
return ErrorType(decodingError: decodingError, data: $1)
return error(from: $0, decoding: ContainerType.self, from: $2)
}
}

private static func error<ErrorType: DecodingFailureInitializable>(from error: Swift.Error, decoding type: Decodable.Type, from data: Data) -> ErrorType {
guard let decodingError = error as? DecodingError else { fatalError("JSONDecoder should always throw a DecodingError.") }
return ErrorType(error: decodingError, decoding: type, data: data)
}
}

// MARK: - Request Default Implementations
Expand Down Expand Up @@ -182,7 +185,7 @@ public extension Request {
}
}

// MARK: - Request Default Implementations
// MARK: - Request Default Implementations [Codable]

public extension Request where ResponseType: Decodable, ErrorType: DecodingFailureInitializable {

Expand All @@ -195,34 +198,11 @@ public extension Request where ResponseType: Decodable, ErrorType: DecodingFailu
}
}

// MARK: - Request Default Implementations [EmptyResponse]

public extension Request where ResponseType == EmptyResponse {

func transformData(_ data: Data, serviceSuccess: NetworkServiceSuccess) -> Result<EmptyResponse, ErrorType> {
return .success(EmptyResponse())
}
}

// MARK: - AnyError Conformance to NetworkServiceInitializable

extension AnyError: NetworkServiceFailureInitializable {

public init(networkServiceFailure: NetworkServiceFailure) {
self.init(networkServiceFailure.error)
}

public var networkServiceError: NetworkServiceError {
return (error as? NetworkServiceError) ?? .unknownError
}

public var failureResponse: HTTP.Response? {
return nil
}
}

// MARK: - AnyError Conformance to DecodingFailureInitializable

extension AnyError: DecodingFailureInitializable {
public init(decodingError: DecodingError, data: Data) {
self.init(decodingError)
}
}
38 changes: 38 additions & 0 deletions Tests/DecodingFailureTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// DecodingFailureTests.swift
// Hyperspace-iOS
//
// Created by Will McGinty on 8/27/18.
// Copyright © 2018 Bottle Rocket Studios. All rights reserved.
//

import XCTest
@testable import Hyperspace
import Result

class DecodingFailureTests: XCTestCase {

private struct MockDecodeError: DecodingFailureInitializable {

let error: DecodingError
let type: Decodable.Type
let data: Data

init(error: DecodingError, decoding: Decodable.Type, data: Data) {
self.error = error
self.type = decoding
self.data = data
}
}

func test_DecodingFailure_CatchesFailedTypeInformation() {
let objectJSON = loadedJSONData(fromFileNamed: "DateObject")

let transformer: (Data) -> Result<MockDecodableContainer, MockDecodeError> = RequestDefaults.dataTransformer(for: JSONDecoder())
let result = transformer(objectJSON)

guard let error = result.error else { XCTFail("The decode should fail."); return }
XCTAssertEqual(String(describing: error.type), String(describing: MockDecodableContainer.self))
XCTAssertEqual(error.data, objectJSON)
}
}
7 changes: 0 additions & 7 deletions Tests/DecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,5 @@ class DecodingTests: XCTestCase {
XCTAssertEqual(stringConvertible.description, obj.description)
}
// swiftlint:enable syntactic_sugar

// MARK: - Helper

private func loadedJSONData(fromFileNamed name: String) -> Data {
let bundle = Bundle(for: DecodingTests.self)
let url = bundle.url(forResource: name, withExtension: "json")!
return try! Data(contentsOf: url)
}
}
5 changes: 3 additions & 2 deletions Tests/Helper/Mocks/MockBackendService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import Foundation
import Result

public enum MockBackendServiceError: NetworkServiceFailureInitializable, DecodingFailureInitializable {

case networkError(NetworkServiceError, HTTP.Response?)
case dataTransformationError(Error)

public init(networkServiceFailure: NetworkServiceFailure) {
self = .networkError(networkServiceFailure.error, networkServiceFailure.response)
}

public init(decodingError: DecodingError, data: Data) {
self = .dataTransformationError(decodingError)
public init(error: DecodingError, decoding: Decodable.Type, data: Data) {
self = .dataTransformationError(error)
}

public var networkServiceError: NetworkServiceError {
Expand Down
18 changes: 18 additions & 0 deletions Tests/Helper/XCTestCase+JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// XCTestCase+JSON.swift
// Hyperspace-iOS
//
// Created by Will McGinty on 9/24/18.
// Copyright © 2018 Bottle Rocket Studios. All rights reserved.
//

import XCTest

extension XCTestCase {

func loadedJSONData(fromFileNamed name: String) -> Data {
let bundle = Bundle(for: DecodingTests.self)
let url = bundle.url(forResource: name, withExtension: "json")!
return try! Data(contentsOf: url)
}
}