Skip to content

HTTPClient now handles response deserialization + Introduced NetworkError and BackendError#1425

Merged
NachoSoto merged 5 commits into
mainfrom
wip-decodable-3-rebased
Apr 12, 2022
Merged

HTTPClient now handles response deserialization + Introduced NetworkError and BackendError#1425
NachoSoto merged 5 commits into
mainfrom
wip-decodable-3-rebased

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Mar 29, 2022

Copy link
Copy Markdown
Contributor

Closes #695 and finishes CF-195.
Depends on #1431, #1432, #1433, #1437.

Goals:

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.
  • 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
  • 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
  • Converted DNSError into NetworkError.dnsError. Its functionality remains unchanged.
  • Removed Backend.RCSuccessfullySyncedKey and ErrorDetails.finishableKey in favor of tested properties on NetworkError

Leftovers

There's a few things I've decided to not finish for now that we can turn into Jiras:

@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch 3 times, most recently from 03f538c to d3889a0 Compare March 30, 2022 22:26
@NachoSoto NachoSoto changed the title [WIP] Continuing HTTPClient refactor [skip-ci] [WIP] Continuing HTTPClient refactor Mar 30, 2022
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch 4 times, most recently from 9d13df9 to ba0096d Compare March 31, 2022 16:19
Comment thread Sources/Networking/HTTPClient.swift Outdated
Comment on lines 40 to 44

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is working now 🎉

@NachoSoto NachoSoto added the WIP label Mar 31, 2022
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch 4 times, most recently from 79f572c to c0a30be Compare March 31, 2022 18:24
@NachoSoto NachoSoto changed the base branch from main to error-response-in-http-client March 31, 2022 18:27
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from c0a30be to d0c54b6 Compare March 31, 2022 18:32
@NachoSoto NachoSoto changed the title [skip-ci] [WIP] Continuing HTTPClient refactor [WIP] HTTPClient now handles response deserialization Mar 31, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gone in favor of the Decodable ETagManager.Response`.

Comment thread Sources/Networking/HTTPClient.swift Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread Sources/Networking/HTTPClient.swift Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

will this be a part of a different PR or this one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Working on it in #1437, that's the last piece to finish.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is officially gone in #1437 .

Comment thread Sources/Networking/HTTPClient.swift Outdated
Comment on lines 191 to 209

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All this code is now gone 🎉

Comment thread Sources/Networking/HTTPClient.swift Outdated
Comment on lines 385 to 396

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 ResponseBody instead of just Value so 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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`
            }
        }
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread Sources/Networking/HTTPResponse.swift Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the main change in this PR.

Comment thread Sources/Networking/HTTPResponse.swift Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The new implementation still allows extracting a plain [String: Any] to maintain compatibility for types that aren't Decodable yet.

Comment on lines 24 to 27

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is super simple now. If there's no attribute errors, return customer info. If there are attribute errors, return error.

Comment on lines 44 to 45

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Composition at its best. Decoding 2 different types to create one 🌮

@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 7bfbd17 to 0e34e67 Compare April 1, 2022 21:30
@NachoSoto NachoSoto removed the WIP label Apr 1, 2022
Base automatically changed from error-response-in-http-client to main April 5, 2022 18:39
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 0e34e67 to 87a2c90 Compare April 5, 2022 18:57
@NachoSoto

Copy link
Copy Markdown
Contributor Author

Planning to finish #1437 to wrap this up this week.

@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 87a2c90 to 27a3ea3 Compare April 6, 2022 21:35
Comment thread Sources/Identity/CustomerInfo.swift Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤔 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤔 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.

Comment thread Sources/Identity/CustomerInfo.swift Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is there a PR for this bit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Working on that in #1496.

Comment thread Sources/Networking/ETagManager.swift Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 is there a PR for a jira for it?

Comment thread Sources/Networking/HTTPClient.swift Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

will this be a part of a different PR or this one?

Comment thread Sources/Networking/HTTPClient.swift Outdated
Comment thread Sources/Networking/HTTPClient.swift Outdated
Comment on lines 385 to 396

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 ResponseBody instead of just Value so 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?

@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from a6e6d43 to 826abb8 Compare April 8, 2022 01:23
@NachoSoto NachoSoto requested a review from taquitos April 8, 2022 01:25
@NachoSoto

Copy link
Copy Markdown
Contributor Author

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.

@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 4a6ba22 to 7445957 Compare April 8, 2022 21:36
@NachoSoto NachoSoto requested a review from aboedo April 8, 2022 21:41
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 7445957 to 8e0c0be Compare April 8, 2022 22:49
@NachoSoto

Copy link
Copy Markdown
Contributor Author

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.

NachoSoto and others added 4 commits April 12, 2022 12:45
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`.
We're not ready to get rid of this method yet
@NachoSoto NachoSoto force-pushed the wip-decodable-3-rebased branch from 8e0c0be to 1f28630 Compare April 12, 2022 19:45
@aboedo

aboedo commented Apr 12, 2022

Copy link
Copy Markdown
Member

@NachoSoto perhaps we can wait until #1437 so we can get tests passing first?

@NachoSoto

Copy link
Copy Markdown
Contributor Author

Sure, you want me to merge that in here once it's approved? It depends on this branch.

@aboedo

aboedo commented Apr 12, 2022

Copy link
Copy Markdown
Member

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`
@NachoSoto NachoSoto changed the title HTTPClient now handles response deserialization HTTPClient now handles response deserialization + Introduced NetworkError and BackendError Apr 12, 2022
@NachoSoto

Copy link
Copy Markdown
Contributor Author

Done! This PR is now completely functional.

@NachoSoto NachoSoto merged commit 23f63c5 into main Apr 12, 2022
@NachoSoto NachoSoto deleted the wip-decodable-3-rebased branch April 12, 2022 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor HTTPClient

2 participants