HTTPClient now handles response deserialization + Introduced NetworkError and BackendError#1425
Conversation
03f538c to
d3889a0
Compare
HTTPClient refactorHTTPClient refactor
9d13df9 to
ba0096d
Compare
There was a problem hiding this comment.
This is working now 🎉
79f572c to
c0a30be
Compare
c0a30be to
d0c54b6
Compare
HTTPClient refactorHTTPClient now handles response deserialization
There was a problem hiding this comment.
Gone in favor of the Decodable ETagManager.Response`.
There was a problem hiding this comment.
This was the key. To be able to store a type erased Request, the completion handler just deals with the Data itself. The constructor is the only thing that has the Value type information, so it handles the deserialization there so everything else in HTTPClient is type agnostic.
There was a problem hiding this comment.
This is one of the last TODOs, when the response fails to deserialize I need to improve how the error is created. Now the CodableError is what's forwarded which isn't ideal.
There was a problem hiding this comment.
will this be a part of a different PR or this one?
There was a problem hiding this comment.
Working on it in #1437, that's the last piece to finish.
There was a problem hiding this comment.
All this code is now gone 🎉
There was a problem hiding this comment.
I understand this code might be hard to follow, it's peak "functional" code. Ultimately it's just converting from Data to Value by calling the HTTPResponseBody decoding method. The nesting is because it's inside of the hierarchy of types of Result and HTTPResponse.
Feedback welcome if there's objections about it.
There was a problem hiding this comment.
Honestly, I had a tough time understanding this. I think the mixture of functional methods, generic types, conversion of result types, Result.init with the block passed in directly, deep nesting, and implicit returns was a bit too much for my brain to handle.
Thoughts on making it easier to understand:
- Adding explicit types so that the conversions are easier to follow
- Making the result.init call pass in the block with the name
- using
ResponseBodyinstead of justValueso the type is easier to keep track of
// Parses a `Result<HTTPResponse<Data>>` to `Result<HTTPResponse<Value>>`
func parseResponse<ResponseBody: HTTPResponseBody>() -> HTTPResponse<ResponseBody>.Result {
return self.flatMap { (response: HTTPResponse<Data>) in
HTTPResponse<ResponseBody>.Result.init(catching: {
try response.mapBody { (data: Data) in
try ResponseBody.create(with: data) // build HTTPResponseBody
}
})
}
}But even with those changes this is hard to read for me. @taquitos thoughts on this?
There was a problem hiding this comment.
Honestly, I had a tough time understanding this. I think the mixture of functional methods, generic types, conversion of result types, Result.init with the block passed in directly, deep nesting, and implicit returns was a bit too much for my brain to handle.
Totally fair.
How about explaining what each line does in English (plus maybe adding all explicit types)?
// Parses a `Result<HTTPResponse<Data>>` to `Result<HTTPResponse<Value>>`
func parseResponse<Value: HTTPResponseBody>() -> HTTPResponse<Value>.Result {
return self.flatMap { response in // Convert the `Result` type
HTTPResponse<Value>.Result { // Create a new `HTTPResponse<Value>`
try response.mapBody { data in // Convert the body of `HTTPResponse<Data>` from `Data` -> `Value`
try Value.create(with: data) // Decode `Data` into `Value`
}
}
}
}There was a problem hiding this comment.
Something to comment on this, is that this code is doing just a data transformation, it's not like it has some weird logic that needs to be maintained. There really is only one way to implement this, maybe many ways of writing it, but they would all compile to the same thing.
There was a problem hiding this comment.
This is the main change in this PR.
There was a problem hiding this comment.
This typealias is 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 underlyingErrors.
There was a problem hiding this comment.
The new implementation still allows extracting a plain [String: Any] to maintain compatibility for types that aren't Decodable yet.
There was a problem hiding this comment.
This is super simple now. If there's no attribute errors, return customer info. If there are attribute errors, return error.
There was a problem hiding this comment.
Composition at its best. Decoding 2 different types to create one 🌮
7bfbd17 to
0e34e67
Compare
0e34e67 to
87a2c90
Compare
|
Planning to finish #1437 to wrap this up this week. |
87a2c90 to
27a3ea3
Compare
There was a problem hiding this comment.
🤔 is this necessary? I'm wondering if this might make it trickier for customers to use our classes for testing if they're doing inherit+replace
There was a problem hiding this comment.
🤔 is this necessary?
It is to be able to use Self in the HTTPResponseBody implementation. I could make that explicitly use CustomerInfo, but.
I'm wondering if this might make it trickier for customers to use our classes for testing if they're doing inherit+replace
None of our classes are open. Since Swift 3, Swift classes are sealed (non-open) by default. So this being final only affects the SDK itself.
There was a problem hiding this comment.
There's not, I can create a Jira for it.
It's one of the last classes that still needs the conversion. That's why I've made the new system still work with manual serialization.
There was a problem hiding this comment.
👍 is there a PR for a jira for it?
There was a problem hiding this comment.
will this be a part of a different PR or this one?
There was a problem hiding this comment.
Honestly, I had a tough time understanding this. I think the mixture of functional methods, generic types, conversion of result types, Result.init with the block passed in directly, deep nesting, and implicit returns was a bit too much for my brain to handle.
Thoughts on making it easier to understand:
- Adding explicit types so that the conversions are easier to follow
- Making the result.init call pass in the block with the name
- using
ResponseBodyinstead of justValueso the type is easier to keep track of
// Parses a `Result<HTTPResponse<Data>>` to `Result<HTTPResponse<Value>>`
func parseResponse<ResponseBody: HTTPResponseBody>() -> HTTPResponse<ResponseBody>.Result {
return self.flatMap { (response: HTTPResponse<Data>) in
HTTPResponse<ResponseBody>.Result.init(catching: {
try response.mapBody { (data: Data) in
try ResponseBody.create(with: data) // build HTTPResponseBody
}
})
}
}But even with those changes this is hard to read for me. @taquitos thoughts on this?
a6e6d43 to
826abb8
Compare
|
The tests and lint are fixed in #1437, but I don't want to merge it here because it would make this even longer to review. |
4a6ba22 to
7445957
Compare
7445957 to
8e0c0be
Compare
|
Any pending comments here? Let me know if you prefer me to merge #1437 in here to take a final look at all together, or you're ok with me merging them separately. |
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`.
…ormat fails gracefully (#1438)
We're not ready to get rid of this method yet
8e0c0be to
1f28630
Compare
|
@NachoSoto perhaps we can wait until #1437 so we can get tests passing first? |
|
Sure, you want me to merge that in here once it's approved? It depends on this branch. |
|
sure! #1437 is approved so we should be ready to go |
## Changes:
- Each layer (only a few for now) has its own error type: `NetworkError`, `BackendError`, `OfferingsManager.Error`.
- `HTTPClient` for example has to produce `NetworkError`, operations produce `BackendError`
```diff
struct HTTPResponse<Body: HTTPResponseBody> {
- typealias Result = Swift.Result<Self, Error>
+ typealias Result = Swift.Result<Self, NetworkError>
}
```
- The parent `BackendError` can have specific errors like `.missingAppUserID`, but also be simply a child error `case networkError(NetworkError)`
- All of these conform to a `ErrorCodeConvertible`, so there is a single point of code that converts from simple and readable errors (like `BackendError.emptySubscriberAttributes`, `.unexpectedBackendResponse(.loginResponseDecoding)`) into errors with all the context using `ErrorUtils`
- Tests also become simpler:
```diff
- expect(receivedError?.domain).toEventually(equal(RCPurchasesErrorCodeDomain))
- expect(receivedError?.code).toEventually(
- equal(ErrorCode.unexpectedBackendResponseError.rawValue))
- expect(receivedUnderlyingError?.code).toEventually(
- equal(UnexpectedBackendResponseSubErrorCode.postOfferIdMissingOffersInResponse.rawValue))
+
+ expect(receivedError) == .unexpectedBackendResponse(.postOfferIdMissingOffersInResponse)
```
- Converted `DNSError` into `NetworkError.dnsError`. Its functionality remains unchanged.
- Removed `Backend.RCSuccessfullySyncedKey` and `ErrorDetails.finishableKey` in favor of tested properties on `NetworkError`
HTTPClient now handles response deserializationHTTPClient now handles response deserialization + Introduced NetworkError and BackendError
|
Done! This PR is now completely functional. |
Closes #695 and finishes CF-195.
Depends on
#1431, #1432, #1433, #1437.Goals:
HTTPClient. Clients ofHTTPClientwill only have to handle the "happy" path: Moved response error and status checking toHTTPClient#1431HTTPClientshouldn't returnResult.successwith failed responses, forcing clients to verify the response is actually a failure: Moved response error and status checking toHTTPClient#1431ErrorResponseto abstract error deserialization #1427ErrorResponseto abstract error deserialization #1427HTTPClientbased on aResponseType: Decodabletype, so the completion block will simply return aResult<HTTPResponse<ResponseType>, Error>ETagManagershould store responseDatainstead of a deserialized[String: Any]HTTPClientto fix tests: IntroducedNetworkErrorandBackendError#1437ETagManager: added test to verify that reading cached data in old format fails gracefully #1438Changes:
HTTPResponse's body from[String: Any]to a genericHTTPResponseBody.HTTPResponseBodyto abstractDecodableand provide some default implementations for types likeData,[String: Any](for backwards compatibility to types that aren'tDecodableyet), andDecodableitself.HTTPResponse.Resulttypealias (Result<HTTPResponse<HTTPResponseBody>, Error>) used everywhere. This will allow replacingErrorwith a more specificErrorso 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 inunderlyingError.NetworkError,BackendError,OfferingsManager.Error.HTTPClientfor example has to produceNetworkError, operations produceBackendErrorBackendErrorcan have specific errors like.missingAppUserID, but also be simply a child errorcase networkError(NetworkError)ErrorCodeConvertible, so there is a single point of code that converts from simple and readable errors (likeBackendError.emptySubscriberAttributes,.unexpectedBackendResponse(.loginResponseDecoding)) into errors with all the context usingErrorUtilsDNSErrorintoNetworkError.dnsError. Its functionality remains unchanged.Backend.RCSuccessfullySyncedKeyandErrorDetails.finishableKeyin favor of tested properties onNetworkErrorLeftovers
There's a few things I've decided to not finish for now that we can turn into Jiras:
CustomerInfostill does't conform toDecodable(manual deserialization is still supported by the current system): ConvertedCustomerInfoand related types to useCodable#1496SubscriberAttributeDictcould also beCodableOfferingsFactorywithDecodable:OfferingsManager/OfferingsFactory: usingOfferingsResponse#1435