Skip to content

HTTPClient.perform: changed response to Result<[String: Any], Error>#1387

Merged
NachoSoto merged 2 commits into
mainfrom
http-client-result
Mar 15, 2022
Merged

HTTPClient.perform: changed response to Result<[String: Any], Error>#1387
NachoSoto merged 2 commits into
mainfrom
http-client-result

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

Currently pretty much all closures in the codebase use (Value?, Error?), which is a leftover from Objective-C days. These can represent 4 different cases:

  • Value / no-error
  • No-value / error
  • Value / error
  • No-value / no-error

Out of those 4, only 2 are really valid, which meant that a lot of code was spent handling the other 2, either creating errors (like UnexpectedBackendResponseSubErrorCode.loginMissingResponse) that weren't possible, or hard to follow code because both a value and an error were possible.

In tests, too, the mocks were initially set up to produce no value and no error, which were in many cases triggering code paths that weren't legal with real code.

This change removes this possibility all together by ensuring at compile time, that one of the two is always present.

This is step 1 in a series of refactors.

  • Step 2 will move a lot of duplicated code that handles status codes != 20x, error handling, parsing error responses, etc. up to HTTPClient and out of the operations, making the operations a lot simpler.
  • Step 3 will change HTTPClient to return a Response: Decodable instead of [String: Any], moving more of the response serialization logic to HTTPClient, and leaving the operations to only focus on the "happy path".

@NachoSoto

Copy link
Copy Markdown
Contributor Author

I'm seeing these failures locally too, but only sometimes 🤔 not when I run each test individually. It's failing to find the active UIWindowScene. I debugged this, and the scene is in "background" mode.

I have an idea for how to improve this for tests by I don't understand what in this PR would make these fail all of a sudden?

@NachoSoto

Copy link
Copy Markdown
Contributor Author

Tests fixed in #1390

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 so much logic here that's going to go away and move to HTTPClient.

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 line is basically repeated in all operations, so it'll be moved to HTTPClient too.

Comment thread Purchases/Purchasing/Purchases.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.

Obviously the public APIs still have the Obj-C style API. This is how the Result is converted back.

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.

I'd love to add Result alternatives down the line for Swift, although tbh I imagine a lot of folks will just migrate to async / await

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.

Yeah exactly, but we could easily offer them too

Comment thread PurchasesTests/Mocks/MockBackend.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.

One example of several where tests were using mocks that returned no value and no error by default, which many tests didn't override.

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.

These are no longer using the test-only .init(testData:). The actual method in the code .init(data:) already has a throws initializer, so don't need to use XCTUnwrap. I think using this we can probably get rid of init(testData:).

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 testing something that couldn't happen.

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.

sayonara

@NachoSoto NachoSoto Mar 12, 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.

Same here, why have an Optional around when we can make sure at initialization that the data is correct. Th (Value?, Error?) APIs enabled optionals all over the place that shouldn't have been.

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.

Same here, this could have been setting nil as the mock without knowing 🤷🏻‍♂️

Comment on lines 274 to 310

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.

Again testing something that wasn't possible.

@aboedo aboedo left a comment

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.

awesome!!! I love this refactor. I'd love to see pretty much everything in our codebase that requires completion blocks use Result, and this gets us like 90% of the way there 👏

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.

👏

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.

I've been dreaming of this refactor since we first started migrating code to swift

Comment on lines 65 to 68

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.

tbh I still feel like the needsToRefresh variable made this slightly more readable, i.e.: it told you why you're going on this path - you need to refresh.

maybe we could have this be guard !needsToRefresh else {

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 reason I changed it is so that I could do let customerInfo = instead of cachedCustomerInfo != nil and then force-unwrapping :/

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.

👍 that makes sense

Comment thread Purchases/Identity/IdentityManager.swift Outdated
Comment thread Purchases/Networking/Backend.swift Outdated
Comment on lines 17 to 22

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.

😍

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.

this is such a nice win - now if the completion was called, there's no way that receivedResult could be nil, so need for an extra var

Comment thread PurchasesTests/Networking/HTTPClientTests.swift Outdated
Comment thread PurchasesTests/Networking/HTTPClientTests.swift Outdated
Comment thread PurchasesTests/Networking/HTTPClientTests.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.

sayonara

@NachoSoto NachoSoto force-pushed the http-client-result branch from 7b3b9f1 to af3f089 Compare March 15, 2022 19:33
Currently pretty much all closures in the codebase use `(Value?, Error?)`, which is a leftover from Objective-C days. These can represent 4 different cases:
- **Value / no-error**
- **No-value / error**
- _Value / error_
- _No-value / no-error_

Out of those 4, only 2 are really valid, which meant that a lot of code was spent handling the other 2, either creating errors (like `UnexpectedBackendResponseSubErrorCode.loginMissingResponse`) that weren't possible, or hard to follow code because both a value and an error were possible.

In tests, too, the mocks were initially set up to produce no value and no error, which were in many cases triggering code paths that weren't legal with real code.

This change removes this possibility all together by ensuring **at compile time**, that one of the two is always present.
@NachoSoto NachoSoto force-pushed the http-client-result branch from af3f089 to b812108 Compare March 15, 2022 19:56
@NachoSoto NachoSoto requested a review from aboedo March 15, 2022 20:01
@NachoSoto NachoSoto merged commit dc8edc4 into main Mar 15, 2022
@NachoSoto NachoSoto deleted the http-client-result branch March 15, 2022 20:17
NachoSoto added a commit that referenced this pull request Mar 15, 2022
These have began failing on #1387, even though that doesn't change anything that should affect the `UIWindowScene` or anything related. This could have been a regression from Xcode 13.3.
When the test fails (which isn't always), the scene isn't yet `.foregroundActive` . To workaround that, this defaults to any scene, *though only when running tests, on debug, and on the simulator*. So nothing is changed in release or device builds.

This also has a small refactor on `BeginRefundRequestHelperTests` to move the test data below and make the tests more readable.
Additionally, the `MockSK2BeginRefundRequestHelper` is recreated for every test. I thought initially that this is what was making the test fail, as the mock expectations and state was being shared across tests. It's not, but now that should remove any potential issues with shared state.
NachoSoto added a commit that referenced this pull request Mar 15, 2022
These have began failing on #1387, even though that doesn't change anything that should affect the `UIWindowScene` or anything related. This could have been a regression from Xcode 13.3.
When the test fails (which isn't always), the scene isn't yet `.foregroundActive` . To workaround that, this defaults to any scene, *though only when running tests, on debug, and on the simulator*. So nothing is changed in release or device builds.

This also has a small refactor on `BeginRefundRequestHelperTests` to move the test data below and make the tests more readable.
Additionally, the `MockSK2BeginRefundRequestHelper` is recreated for every test. I thought initially that this is what was making the test fail, as the mock expectations and state was being shared across tests. It's not, but now that should remove any potential issues with shared state.
NachoSoto added a commit that referenced this pull request Jul 28, 2022
… if the response was successful

Fixes [SDKONCALL-62].
The behavior in version 3.x was that we would return a `CustomerInfo` AND the attribute errors. [The test in that version](https://github.com/RevenueCat/purchases-ios/blob/3.14.2/PurchasesTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift#L370-L404) however only verified that the error was returned.

When we converted `(Value, Error)` callbacks to using `Result` (#1387), I assumed this was an error. Because of that incomplete test, we didn't notice this wasn't the desired behavior.

If the backend returns a 200, it's assumed that the errors aren't more important than the correct `CustomerInfo`.

The new implementation simply logs the attribute errors (which is now tested), and returns the `CustomerInfo`.
NachoSoto added a commit that referenced this pull request Jul 29, 2022
… if the response was successful (#1778)

Fixes [SDKONCALL-62].
The behavior in version 3.x was that we would return a `CustomerInfo` AND the attribute errors. [The test in that version](https://github.com/RevenueCat/purchases-ios/blob/3.14.2/PurchasesTests/SubscriberAttributes/BackendSubscriberAttributesTests.swift#L370-L404) however only verified that the error was returned.

When we converted `(Value, Error)` callbacks to using `Result` (#1387), I assumed this was an error. Because of that incomplete test, we didn't notice this wasn't the desired behavior.

If the backend returns a 200, it's assumed that the errors aren't more important than the correct `CustomerInfo`.

The new implementation simply logs the attribute errors (which is now tested), and returns the `CustomerInfo`:
```
😿‼️ One or more of the attributes sent could not be saved. ["$email": "email is not in valid format"]
```

[SDKONCALL-62]: https://revenuecats.atlassian.net/browse/SDKONCALL-62?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
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.

2 participants