-
Notifications
You must be signed in to change notification settings - Fork 225
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
Conversation
53191a5
to
397f96d
Compare
/cc @grigoryk for visibility into this work. I currently have a small diff to a-c to ensure that |
a663055
to
e64915e
Compare
Codecov Report
@@ Coverage Diff @@
## main #3876 +/- ##
======================================
Coverage 0.00% 0.00%
======================================
Files 174 174
Lines 12257 12081 -176
======================================
+ Misses 12257 12081 -176
Continue to review full report at Codecov.
|
fb5d407
to
9f5dfaf
Compare
7cd10f0
to
ccaf1f3
Compare
0274c82
to
e22c368
Compare
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/" |
There was a problem hiding this comment.
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
@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) |
There was a problem hiding this 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.
components/fxa-client/README.md
Outdated
identity service. | ||
|
||
* [Features](#features) | ||
* [Using the Firefox Accounts client](#using-the-logins-component) |
There was a problem hiding this comment.
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.
components/fxa-client/README.md
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - use 1
consistently?
components/fxa-client/README.md
Outdated
### 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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... |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo in Disconenct
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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::{ |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/. */ |
There was a problem hiding this comment.
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! |
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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.
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? |
IMO followups is fine (and TBH I'm not too bothered even then - they were more off-the-cuff comments) |
@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. |
Just noting for completeness, I've pushed some tweaks and this is ready for formal r?. |
63559bb
to
945eb0e
Compare
The failing tests here are the sync integration tests, which are failing due to |
There was a problem hiding this 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!
components/fxa-client/README.md
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: stray trailing period.
components/fxa-client/README.md
Outdated
1. Listing and managing other applications connected to the | ||
user's account. | ||
1. Sending tabs to other applications that are capable | ||
of receiving them. |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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() } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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.") |
There was a problem hiding this comment.
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`: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"...
There was a problem hiding this comment.
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.
3229719
to
e26cd01
Compare
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.
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: