Skip to content

Commit

Permalink
HTTPClient now handles response deserialization
Browse files Browse the repository at this point in the history
Closes #695.
Depends on #1431, #1432, #1433.

## Changes:
- Replaced `HTTPResponse`'s body from `[String: Any]` to a generic `HTTPResponseBody`.
- Created `HTTPResponseBody` to abstract `Decodable` and provide some default implementations for types like `Data,` `[String: Any]` (for backwards compatibility to types that aren't `Decodable` yet), and `Decodable` itself.
- New `HTTPResponse.Result` typealias (`Result<HTTPResponse<HTTPResponseBody>, Error>`) used everywhere. This will allow replacing `Error` with a more specific `Error` so we can forward known typed errors, and make sure that we don't end up with the wrong error type, or with a very complex error hierarchy and the details buried in `underlyingError`.
  • Loading branch information
NachoSoto committed Apr 6, 2022
1 parent 6dfc996 commit 27a3ea3
Show file tree
Hide file tree
Showing 27 changed files with 543 additions and 317 deletions.
8 changes: 4 additions & 4 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@
35D83312262FBD4200E60AC5 /* MockETagManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D83311262FBD4200E60AC5 /* MockETagManager.swift */; };
35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */; };
35E840CE2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; };
35F6FD62267426D600ABCB53 /* ETagAndResponseWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */; };
35F82BAB26A84E130051DF03 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */; };
35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */; };
35F82BB426A9A74D0051DF03 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */; };
Expand All @@ -218,6 +217,7 @@
5746508C27586B2E0053AB09 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5746508B27586B2E0053AB09 /* Result+Extensions.swift */; };
5746508E275949F20053AB09 /* DispatchTimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5746508D275949F20053AB09 /* DispatchTimeInterval+Extensions.swift */; };
5751379527F4C4D80064AB2C /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5751379427F4C4D80064AB2C /* Optional+Extensions.swift */; };
575137CF27F50D2F0064AB2C /* HTTPResponseBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575137CE27F50D2F0064AB2C /* HTTPResponseBody.swift */; };
57536A2627851FFE00E2AE7F /* SK1StoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57536A2527851FFE00E2AE7F /* SK1StoreTransaction.swift */; };
57536A28278522B400E2AE7F /* SK2StoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57536A27278522B400E2AE7F /* SK2StoreTransaction.swift */; };
575A17AB2773A59300AA6F22 /* CurrentTestCaseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */; };
Expand Down Expand Up @@ -576,7 +576,6 @@
35E1CE1F26E022C20008560A /* TrialOrIntroPriceEligibilityCheckerSK1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialOrIntroPriceEligibilityCheckerSK1Tests.swift; sourceTree = "<group>"; };
35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsHelper.swift; sourceTree = "<group>"; };
35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManageSubscriptionsHelper.swift; sourceTree = "<group>"; };
35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETagAndResponseWrapper.swift; sourceTree = "<group>"; };
35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = "<group>"; };
35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionDataMigratorTests.swift; sourceTree = "<group>"; };
35F82BB326A9A74D0051DF03 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -642,6 +641,7 @@
5746508B27586B2E0053AB09 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = "<group>"; };
5746508D275949F20053AB09 /* DispatchTimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchTimeInterval+Extensions.swift"; sourceTree = "<group>"; };
5751379427F4C4D80064AB2C /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
575137CE27F50D2F0064AB2C /* HTTPResponseBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponseBody.swift; sourceTree = "<group>"; };
57536A2527851FFE00E2AE7F /* SK1StoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SK1StoreTransaction.swift; sourceTree = "<group>"; };
57536A27278522B400E2AE7F /* SK2StoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SK2StoreTransaction.swift; sourceTree = "<group>"; };
575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTestCaseTracker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1384,14 +1384,14 @@
B34605AA279A6E380031CA74 /* Operations */,
B3C4AAD426B8911300E1B3C8 /* Backend.swift */,
B3F3E8D9277158FE0047A5B9 /* DNSChecker.swift */,
35F6FD61267426D600ABCB53 /* ETagAndResponseWrapper.swift */,
35D832CC262A5B7500E60AC5 /* ETagManager.swift */,
35F82BB326A9A74D0051DF03 /* HTTPClient.swift */,
57DC9F4527CC2E4900DA6AF9 /* HTTPRequest.swift */,
35D832F3262E606500E60AC5 /* HTTPResponse.swift */,
35D832D1262E56DB00E60AC5 /* HTTPStatusCode.swift */,
B3766F1D26BDA95100141450 /* IntroEligibilityResponse.swift */,
B34605D0279A6E600031CA74 /* SubscribersAPI.swift */,
575137CE27F50D2F0064AB2C /* HTTPResponseBody.swift */,
);
path = Networking;
sourceTree = "<group>";
Expand Down Expand Up @@ -2093,7 +2093,6 @@
2DC5623024EC63730031F69B /* OperationDispatcher.swift in Sources */,
57A0FBF22749CF66009E2FC3 /* SynchronizedUserDefaults.swift in Sources */,
F5714EE526DC2F1D00635477 /* CodableStrings.swift in Sources */,
35F6FD62267426D600ABCB53 /* ETagAndResponseWrapper.swift in Sources */,
57C381DC27961547009E3940 /* SK2StoreProductDiscount.swift in Sources */,
B34605CA279A6E380031CA74 /* GetCustomerInfoOperation.swift in Sources */,
5751379527F4C4D80064AB2C /* Optional+Extensions.swift in Sources */,
Expand Down Expand Up @@ -2185,6 +2184,7 @@
9A65E0A02591A23200DE00B0 /* OfferingStrings.swift in Sources */,
B34605D1279A6E600031CA74 /* SubscribersAPI.swift in Sources */,
2DDF41A224F6F331005BC22D /* ProductsManager.swift in Sources */,
575137CF27F50D2F0064AB2C /* HTTPResponseBody.swift in Sources */,
B3AA6236268A81C700894871 /* EntitlementInfos.swift in Sources */,
B372EC56268FEF020099171E /* ProductRequestData.swift in Sources */,
359E8E3F26DEBEEB00B869F9 /* TrialOrIntroPriceEligibilityChecker.swift in Sources */,
Expand Down
14 changes: 14 additions & 0 deletions Sources/FoundationExtensions/JSONDecoder+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ extension KeyedDecodingContainer {

}

extension JSONSerialization {

static func dictionary(withData data: Data) throws -> [String: Any] {
let object = try JSONSerialization.jsonObject(with: data)

guard let object = object as? [String: Any] else {
throw CodableError.unexpectedValue(type(of: object))
}

return object
}

}

// MARK: Decoding Error handling
private extension ErrorUtils {

Expand Down
11 changes: 10 additions & 1 deletion Sources/Identity/CustomerInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Foundation
A container for the most recent customer info returned from `Purchases`.
These objects are non-mutable and do not update automatically.
*/
@objc(RCCustomerInfo) public class CustomerInfo: NSObject {
@objc(RCCustomerInfo) public final class CustomerInfo: NSObject {

/// ``EntitlementInfos`` attached to this customer info.
@objc public let entitlements: EntitlementInfos
Expand Down Expand Up @@ -421,3 +421,12 @@ private extension CustomerInfo {
}

}

// Fixme: delete when `CustomerInfo` is `Decodable`
extension CustomerInfo: HTTPResponseBody {

static func create(with data: Data) throws -> Self {
return try self.init(data: try JSONSerialization.dictionary(withData: data))
}

}
65 changes: 0 additions & 65 deletions Sources/Networking/ETagAndResponseWrapper.swift

This file was deleted.

68 changes: 46 additions & 22 deletions Sources/Networking/ETagManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ class ETagManager {
}

func httpResultFromCacheOrBackend(with response: HTTPURLResponse,
jsonObject: [String: Any],
data: Data?,
request: URLRequest,
retried: Bool) -> HTTPResponse? {
retried: Bool) -> HTTPResponse<Data>? {
let statusCode: HTTPStatusCode = .init(rawValue: response.statusCode)
let resultFromBackend = HTTPResponse(statusCode: statusCode, jsonObject: jsonObject)
let resultFromBackend = HTTPResponse(statusCode: statusCode, body: data)
.asOptionalResponse

let headersInResponse = response.allHeaderFields

let eTagInResponse: String? = headersInResponse[ETagManager.eTagHeaderName] as? String ??
headersInResponse[ETagManager.eTagHeaderName.lowercased()] as? String
headersInResponse[ETagManager.eTagHeaderName.lowercased()] as? String

guard let eTagInResponse = eTagInResponse else { return resultFromBackend }
if shouldUseCachedVersion(responseCode: statusCode) {
Expand All @@ -58,7 +59,7 @@ class ETagManager {
if retried {
Logger.warn(
Strings.network.could_not_find_cached_response_in_already_retried(
response: resultFromBackend.description
response: resultFromBackend?.description ?? ""
)
)
return resultFromBackend
Expand All @@ -68,8 +69,9 @@ class ETagManager {
storeStatusCodeAndResponseIfNoError(
for: request,
statusCode: statusCode,
responseObject: jsonObject,
eTag: eTagInResponse)
data: data,
eTag: eTagInResponse
)
return resultFromBackend
}

Expand All @@ -87,37 +89,29 @@ private extension ETagManager {
responseCode == .notModified
}

func storedETagAndResponse(for request: URLRequest) -> ETagAndResponseWrapper? {
func storedETagAndResponse(for request: URLRequest) -> Response? {
return self.userDefaults.read {
if let cacheKey = eTagDefaultCacheKey(for: request),
let value = $0.object(forKey: cacheKey),
let data = value as? Data {
return ETagAndResponseWrapper(with: data)
return try? JSONDecoder.default.decode(Response.self, jsonData: data)
}

return nil
}
}

func storedHTTPResponse(for request: URLRequest) -> HTTPResponse? {
if let storedETagAndResponse = storedETagAndResponse(for: request) {
return HTTPResponse(
statusCode: storedETagAndResponse.statusCode,
jsonObject: storedETagAndResponse.jsonObject
)
}

return nil
func storedHTTPResponse(for request: URLRequest) -> HTTPResponse<Data>? {
return storedETagAndResponse(for: request)?.asResponse
}

func storeStatusCodeAndResponseIfNoError(for request: URLRequest,
statusCode: HTTPStatusCode,
responseObject: [String: Any]?,
data: Data?,
eTag: String) {
if statusCode != .notModified && !statusCode.isServerError,
let responseObject = responseObject,
if let data = data, statusCode != .notModified && !statusCode.isServerError,
let cacheKey = eTagDefaultCacheKey(for: request) {
let eTagAndResponse = ETagAndResponseWrapper(eTag: eTag, statusCode: statusCode, jsonObject: responseObject)
let eTagAndResponse = Response(eTag: eTag, statusCode: statusCode, data: data)
if let dataToStore = eTagAndResponse.asData() {
self.userDefaults.write {
$0.set(dataToStore, forKey: cacheKey)
Expand All @@ -130,6 +124,8 @@ private extension ETagManager {
return request.url?.absoluteString
}

// TODO: delete old data since this isn't backwards compatible

static let suiteNameBase: String = "revenuecat.etags"
static var suiteName: String {
guard let bundleID = Bundle.main.bundleIdentifier else {
Expand All @@ -139,3 +135,31 @@ private extension ETagManager {
}

}

extension ETagManager {

struct Response {

let eTag: String
let statusCode: HTTPStatusCode
let data: Data

}

}

extension ETagManager.Response: Codable {}

extension ETagManager.Response {

func asData() -> Data? {
return try? JSONEncoder.default.encode(self)
}

fileprivate var asResponse: HTTPResponse<Data> {
return HTTPResponse(
statusCode: self.statusCode,
body: self.data
)
}
}
Loading

0 comments on commit 27a3ea3

Please sign in to comment.