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

Restructure, UniFFI, and document the fxa-client crate. #3876

Merged
merged 1 commit into from
Mar 9, 2021

Conversation

rfk
Copy link
Contributor

@rfk rfk commented Feb 18, 2021

This is a substantial refactor of the fxa-client crate, intended to bring it
up to speed with our latest best-practices around developing cross-platform
Rust components, in order to ease ongoing maintenance.

It's still very much a work in progress, but the setup does seem to be working,
and feedback is always welcome.

[Edit: It's ready for review]

This introduces backwards-incompatible API changes, which I'm tracking in PRs to our downstream consumers here:

@rfk
Copy link
Contributor Author

rfk commented Feb 18, 2021

/cc @grigoryk for visibility into this work. I currently have a small diff to a-c to ensure that service-firefox-account compiles against this restructured version of fxaclient, my next steps are to try it out in a full Fenix build and see how it goes.

@codecov-io
Copy link

codecov-io commented Feb 24, 2021

Codecov Report

Merging #3876 (e26cd01) into main (ee6cb4d) will not change coverage.
The diff coverage is 0.00%.

Impacted file tree graph

@@          Coverage Diff           @@
##            main   #3876    +/-   ##
======================================
  Coverage   0.00%   0.00%            
======================================
  Files        174     174            
  Lines      12257   12081   -176     
======================================
+ Misses     12257   12081   -176     
Impacted Files Coverage Δ
components/fxa-client/src/internal/auth.rs 0.00% <ø> (ø)
components/fxa-client/src/internal/commands/mod.rs 0.00% <0.00%> (ø)
...nents/fxa-client/src/internal/commands/send_tab.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/config.rs 0.00% <ø> (ø)
components/fxa-client/src/internal/device.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/error.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/http_client.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/migrator.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/mod.rs 0.00% <0.00%> (ø)
components/fxa-client/src/internal/oauth.rs 0.00% <0.00%> (ø)
... and 18 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ee6cb4d...e26cd01. Read the comment docs.

@rfk rfk force-pushed the fxa-client-cleanups branch 6 times, most recently from fb5d407 to 9f5dfaf Compare February 25, 2021 04:29
@rfk rfk force-pushed the fxa-client-cleanups branch 2 times, most recently from 7cd10f0 to ccaf1f3 Compare February 25, 2021 06:39
@rfk rfk force-pushed the fxa-client-cleanups branch 6 times, most recently from 0274c82 to e22c368 Compare February 26, 2021 10:45
@rfk rfk marked this pull request as ready for review February 26, 2021 10:46
commandLine 'cargo', 'uniffi-bindgen', 'generate', "${project.projectDir}/../src/fxa_client.udl", '--language', 'kotlin', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"

inputs.file '../src/fxa_client.udl'
outputs.dir "${buildDir}/generated/source/uniffi/${variant.name}/java/"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The inputs/outputs thing here helps gradle know when to re-run this command, and may be worth applying to other uniffied crates; /cc @lougeniaC64 if you want to try it out for autofil

@rfk
Copy link
Contributor Author

rfk commented Feb 28, 2021

@grigoryk I've pinged you for review here for the Kotlin-side changes, if you're happy to take a look. (Your feedback on the whole thing is of course most welcome! But don't feel obliged to dig in to all the Rust changes)

Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

Wow, this is looking great and is a huge improvement! It's a huge diff and I'm only 1/2 way through and need to leave for the day, so might as well submit what I have. I'll look more tomorrow.

identity service.

* [Features](#features)
* [Using the Firefox Accounts client](#using-the-logins-component)
Copy link
Member

Choose a reason for hiding this comment

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

nit: a number of 'logins' references here.

or a QR-code-based pairing flow.
1. Accessing basic profile information about the signed-in user,
such as email address and display name.
2. Obtaining OAuth access tokens and client-side encryption keys.
Copy link
Member

Choose a reason for hiding this comment

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

nit - use 1 consistently?

### Before using this component

This component does not currently integrate with the [Glean SDK](https://mozilla.github.io/glean/book/index.html)
and does not generate any telemetry, so you do not need to request a data-review before using this component.
Copy link
Member

Choose a reason for hiding this comment

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

s/generate/submit/ here? IIUC it does generate some via telemetry.rs

public API surface in Rust, and then generate bindings for Kotlin and Swift. The
code is organized as follows:

* The public API surface is implemented in [`./src/lib.rs`](./src/lib/rs), with matching
Copy link
Member

Choose a reason for hiding this comment

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

While this is great, very little is specific to FxA, so I wonder if there's an opportunity to move it to some other doc, just to avoid other components doing copy-pasta of it and it diverging over time? Or we just do that when we do that next component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. I think it would be good to pull it out into a common doc at some point, but IMHO that's premature until we get most of the components in a similar shape.

/// - Some convenience higher-level datatypes, such as URLs rather than plain Strings.
///
/// Eventually we'd like to move all of this into the underlying Rust code, once UniFFI
/// grows support for these extra features.
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth capturing what enhancements uniffi needs here (ie, links to issues?)

}
}

// TODO: not sure why we switched to a bool API for the Swift wrapper here...
Copy link
Member

Choose a reason for hiding this comment

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

nit: it's not quite clear what this is asking to actually be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heh, yeah, I guess it's not really a TODO so much as a XXX I do not understand the point of this


// TODO: not sure why we switched to a bool API for the Swift wrapper here...

public func migrateFromSessionToken(
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can kill the migration stuff soon? Regardless, I support not doing it as part of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hope to one day need it for Desktop :-)

declarations in [`./src/fxa_client.udl`](./src/fxa_client.udl) to define how it gets
exposed to other languages.
* All the implementation details are written in Rust and can be found under
[`./src/internal/`](./src/internal).
Copy link
Member

Choose a reason for hiding this comment

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

Do you think a convention of internal makes sense anywhere else? We don't have that with nimbus or autofill, where IIUC, the rule is, roughly, "if it's not exposed via the URL, it's internal". Why is it used here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm trying it out.

Broadly, I like having the contents of src/lib.rs mirror the public API exposed to foreign-language consumers, I think it makes it easier to form a consistent mental model of the component. Pushing all the non-public stuff into an internal crate makes that easy, but it might end up feeling a little forced.

For a crate like this, I like internal as a refactoring mechanism, basically push all the existing stuff in there and then we can incrementally clean it up. For crates built from the start to work with UniFFI, maybe it would be a bit pointless.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

For a crate like this, I like internal as a refactoring mechanism, basically push all the existing stuff in there and then we can incrementally clean it up. For crates built from the start to work with UniFFI, maybe it would be a bit pointless.

It's a good idea in that context, but TBH I don't think it makes sense longer term (ie, it should be temporary) and that rust visibility is the right tool, particularly if fully leveraged (eg, pub(super) is probably underused by us).

In particular, I'm mildly concerned that pragmatic compromises might mean moving lots of code from internal just to expose it - or worse, marking code in internal as pub. For example, there are a number of things in places that's only pub because places-utils.rs wants to use it (and ditto autofill), which is unfortunate, but I tend to suck that up and just allow it to be marked pub)

That's weakly held though, particularly as it's not a "battle tested" opinion (although I'm trying to be better at it in autofill)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I think I broadly agree with your point here - Rust has good mechanisms to manage what's public API and what's private, and we should lean on them rather than hack around them.

Apart from the contents of internal/mod.rs, most of what's in internal could quite happily live at the top level of the crate with limited visibility to prevent it from leaking out.

The goal is "don't have a bunch of pub stuff that's visible to Rust callers, but not available to foreign-language consumers" and I'm not too fussed about the implementation details. Which actually, that goal is pretty aligned with the recent email thread about Public API surface and mental models for our components...

// any potentially-state-changing operation.
//
// **⚠️ Warning:** the serialized state may contain encryption keys and access
// tokens that let anyone holding them access the user's data in Firefox Sync.
Copy link
Member

Choose a reason for hiding this comment

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

maybe add "and/or other FxA services" or similar - eg, the send-tab keys will be there too?

AuthorizationInfo check_authorization_status();


// Disconenct from the user's account.
Copy link
Member

Choose a reason for hiding this comment

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

nit: typo in Disconenct

Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

This is a massive patch but with even more massive value - I admit I skipped over some of it, so not formally approving, but will happily spend even more time to give it a formal r+ whenever you ask.

use super::error::*;

#[derive(Clone, Debug)]
pub enum IncomingDeviceCommand {
Copy link
Member

Choose a reason for hiding this comment

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

hrm - so after writing that comment about visibility, I'm a little confused by seeing pub here? Maybe pub(crate) better reflects what's going on?

Copy link
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 things that's currently used by some example crates.

@@ -37,7 +39,7 @@ impl EncryptedSendTabPayload {
}
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
Copy link
Member

Choose a reason for hiding this comment

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

Yes, this makes perfect sense to me and I expect will be a common pattern for our components - eg, what we serialize to/from the sync servers is rarely going to match what we expose over the api.

Re my previous comment - an internal dir that really only had internal structs used for things like deserializing server responses does make more sense, but it makes less sense to see many impl blocks there other than the Into etc impls


use serde_derive::*;

pub use super::http_client::{
Copy link
Member

Choose a reason for hiding this comment

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

as per above comment, 100% agree

@@ -295,11 +293,7 @@ impl FirefoxAccount {
// for the device because the server does not have a `PATCH commands`
// endpoint yet.
#[allow(dead_code)]
Copy link
Member

Choose a reason for hiding this comment

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

I suspect these dead_code annotations are now stale, so we should probably get an issue on file to remove them, as IMO they have value in general. I understand they are often necessary during dev, but generally shouldn't be necessary longer term.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed a couple, and put a couple back that appeared to still be necessary.

@@ -0,0 +1,560 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Copy link
Member

Choose a reason for hiding this comment

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

phew :)

// Aspirationally, I'd like to make it private, so that the public API of the crate
// is entirely the same as the API exposed to consumers via UniFFI. That's not
// possible right now because some of our tests/example use features that we do
// not currently expose to consumers. But we should figure out how to expose them!
Copy link
Member

Choose a reason for hiding this comment

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

That's a great aspiration. As mentioned above, the URL/Guid <-> String type things might require some uniffi changes, or maybe it already has what we need there - but yeah, not sure how much we should try and insist on that.

@@ -4,7 +4,7 @@

use cli_support::prompt::prompt_string;
use dialoguer::Select;
use fxa_client::{device, Config, FirefoxAccount, IncomingDeviceCommand};
use fxa_client::internal::{device, Config, FirefoxAccount, IncomingDeviceCommand};
Copy link
Member

Choose a reason for hiding this comment

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

yeah - some tension I've found here is probably phrased better as "sometimes we want an example to be more like a diagnotic/test utility than an example of real-world usage" - ie, I explicitly want to call internal stuff in my example while not making it public". ie2, sometimes I want my examples to have crate visibility.

@rfk
Copy link
Contributor Author

rfk commented Mar 4, 2021

I think there's ongoing work to be done around managing the visibility of things in this crate, but just to ensure we're on the same page: @mhammond do you think it's worth pursuing more as part of this PR, or doing it in followups over time?

@mhammond
Copy link
Member

mhammond commented Mar 4, 2021

or doing it in followups over time?

IMO followups is fine (and TBH I'm not too bothered even then - they were more off-the-cuff comments)

@rfk
Copy link
Contributor Author

rfk commented Mar 4, 2021

@mhammond sounds good, thanks! I'll take some time to rebase this and address your comments so far, and then let's please consider this ready for official review. It would be good to get it out into Fenix nightly while we're early in the cycle for maximal bake time.

@rfk
Copy link
Contributor Author

rfk commented Mar 5, 2021

Just noting for completeness, I've pushed some tweaks and this is ready for formal r?.

@rfk rfk requested a review from mhammond March 5, 2021 06:20
@rfk rfk force-pushed the fxa-client-cleanups branch 2 times, most recently from 63559bb to 945eb0e Compare March 5, 2021 06:31
@rfk
Copy link
Contributor Author

rfk commented Mar 8, 2021

The failing tests here are the sync integration tests, which are failing due to 502 server errors from the FxA dev server.

Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

🚢 this is great, future me thanks you!

or a QR-code-based pairing flow.
1. Accessing basic profile information about the signed-in user,
such as email address and display name.
1. Obtaining OAuth access tokens and client-side encryption keys.
Copy link
Member

Choose a reason for hiding this comment

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

nit: stray trailing period.

1. Listing and managing other applications connected to the
user's account.
1. Sending tabs to other applications that are capable
of receiving them.
Copy link
Member

Choose a reason for hiding this comment

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

indentation looks odd here (not clear if it matters when rendered, so feel free to ignore if not)

)
}, persistCallback) {
constructor(config: Config, persistCallback: PersistCallback? = null) : this(FirefoxAccount(
config.contentUrl,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add the essence of this as comment?

}

public func completeOAuthFlow(code: String, state: String) throws {
defer { tryPersistState() }
Copy link
Member

Choose a reason for hiding this comment

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

I noticed above that the kotlin bindings previously did tryPersistState() before the call and now do it after - which I thought a little strange, but assumed doing it after was easier to rationalize about, so shrugged. However, here we are doing it before, which seems an unfortunate inconsistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The use of defer here means that we'll always do it at the end of the function call, like a weird finally block.

// If you find yourself modifying this code, you're almost certainly creating a potential data-migration
// problem and should reconsider.

/// `StateV1` was a previous state schema.
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to exist still? It looks like it's very old and we will never hit this case in the wild. (OTOH, no need to block - maybe another ticket which could be good for onboarding?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, filed as #3912

@@ -173,7 +173,7 @@ class FxAccountManagerTests: XCTestCase {
func testAccountRestorationEnsureCapabilitiesNonAuthError() {
class MockAccount: MockFxAccount {
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
throw FirefoxAccountError.network(message: "The WiFi cable is detached.")
throw FxaError.Network(message: "The WiFi cable is detached.")
Copy link
Member

Choose a reason for hiding this comment

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

lol @ this error message - I've never seen it before

// calling code should respond.
//
//
// `Authentication`:
Copy link
Member

Choose a reason for hiding this comment

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

Any reason to not put the comments for each error next to the definition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH I don't recall, but the answer is almost certainly "I could not convince the automagical UDL generator to do that for me"...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha, it was because we currently represent error enum variants in the ComponentInterface by just their string names, rather than as a Variant object, so there was nowhere for me to stash the docs on each individual variant. I'll fix it up by hand.

This is a substantial refactor of the fxa-client crate, intended to bring it
up to speed with our latest best-practices around developing cross-platform
Rust components, in order to ease ongoing maintenance.

THe core change here is that I've deleted the hand-written Kotlin and Swift
bindings, replacing them with autogenerated bindings thanks to UniFFI.

There is still a little bit of hand-written Kotlin, since we have a layer that
automatically manages persistence via a callback. There is still a nontrivial
amount of hand-written Swift, since we have a higher-level state machine built
atop the lower-level fxa-client functionality (such state-machine also exists
for Kotlin, but lives in the android-components repo). If UniFFI works out then
we should look into moving more of that logic into shared Rust code over time.

To support the introduction of UniFFI, I have restructured to Rust crate so
that its public interface deliberately parallels the interface offered to
Kotlin and Swift, and have moved the implementation details into a submodule
named `internal`. It's my opinionated belief that structuring the crate this
way will help us focus on producing a nice consumer API, which is not always
the same thing as producing a nice Rust crate.

On top of that, I've also revamped the documentation in the crate, leaning
in to the use of `cargo doc` as the source of truth for both developer-
and consumer-facing documentation. Let's consider that an experiment and
see how we like it in practice.

Unfortunately, this is a big PR, but I don't think I could have made it
too much smaller. Hopefully it will be easier to review than its size
suggests since it's mostly additions and deletions rather than complex
changes.
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.

4 participants