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

Create a Rust component for a Merino client (#5345) #5346

Closed
wants to merge 12 commits into from

Conversation

linabutler
Copy link
Member

Pull Request checklist

  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGES_UNRELEASED.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due dilligence applied in selecting them.

Branch builds: add [ff-android: firefox-android-branch-name] and/or [fenix: fenix-branch-name] to the PR title.

Each Merino suggestion provider returns a set of base fields, and
additional fields specific to that provider. For example, adM
suggestions have keywords and additional URLs; Top Picks suggestions
have a `block_id`; AccuWeather suggestions (not implemented yet) have a
forecast and current conditions.

This commit reworks the Merino suggestion response model to capture the
base fields in a `MerinoSuggestionDetails` dictionary, and expose
provider-specific fields as associated data for each provider variant.
components/merino_client/Cargo.toml Show resolved Hide resolved
components/merino_client/Cargo.toml Show resolved Hide resolved
megazords/full/Cargo.toml Show resolved Hide resolved
.buildconfig-android.yml Show resolved Hide resolved
components/merino_client/src/client.rs Outdated Show resolved Hide resolved
Copy link
Member Author

@linabutler linabutler left a comment

Choose a reason for hiding this comment

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

@tiftran I left some comments on each commit, with some pointers that might be interesting for you! 😊

components/merino_client/Cargo.toml Show resolved Hide resolved
components/merino_client/src/client.rs Show resolved Hide resolved
components/merino_client/src/client.rs Show resolved Hide resolved
components/merino_client/src/client.rs Show resolved Hide resolved
components/merino_client/src/client.rs Show resolved Hide resolved
};

[Enum]
interface MerinoSuggestion {
Copy link
Member Author

@linabutler linabutler Feb 2, 2023

Choose a reason for hiding this comment

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

note: UDL dictionaries don't support inheritance—and neither do Rust or Swift structs—so we can't do something like dictionary AdmSuggestionDetails : MerinoSuggestionDetails, and have a separate dictionary type for each provider that inherits the base details. That's why the base details are always the first field in each provider variant, followed by the provider-specific fields.

Copy link
Member Author

Choose a reason for hiding this comment

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

thought: Even though we can't have inheritance, I've found it super helpful to have accessors for the details in both Kotlin and Swift:

val MerinoSuggestion.details: MerinoSuggestionDetails
    get() = when (this) {
        is MerinoSuggestion.Adm -> details
        is MerinoSuggestion.TopPicks -> details
        is MerinoSuggestion.Other -> details
    }
extension MerinoSuggestion {
    var details: MerinoSuggestionDetails {
        switch self {
        case .adm(let details, _, _, _, _, _):
            return details
        case .topPicks(let details, _, _):
            return details
        case .other(let details, _):
            return details
        }
    }
}

That lets us write code like suggestion.details.url and suggestion.details.icon, instead of having to pattern-match on the suggestion with when/switch, and extract the details every time we want a base suggestion field.

UniFFI enum interfaces can't have methods or getters, so we can't put that accessor in the UDL—but we can still add hand-written Kotlin and Swift source files with those extensions to our component! The FxA client does something like that in its Kotlin and Swift bindings.

This would be a great exercise for getting more familiar with the component directory structure, and updating the Xcode project to include the hand-written Swift extension!

/cc @tiftran

Copy link
Member

Choose a reason for hiding this comment

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

thought: Even though we can't have inheritance, I've found it super helpful to have accessors for the details in both Kotlin and Swift:

That's really nice! I wonder if we can teach UniFFI about that?

UniFFI enum interfaces can't have methods or getters, so we can't put that accessor in the UDL—but we can still add hand-written Kotlin and Swift source files with those extensions to our component!

This raises an interesting point. The hand-written foreign code next to the component was initially hugely valuable - our hand-written FFI was so painful that wrappers made a lot of sense. However, as UniFFI means the bindings are quite ergonomic, the app-services team has had an informal decision to keep these hand-wrappers as small as possible - if at all. Part of the reason was that we found understanding old code became quite difficult, particularly for Fenix - by the time our component ended up being consumed we were often many levels of indirection away from the component itself - we have our hand-wrappers, a couple of wrappers in android-components (often a "concept" and a "service"), then the final consumer. Nimbus has gone the other way (the "canonical" wrappers are next to their component). I don't think there is a single correct answer here, but it is a tension that we still feel.

(One example of what I'm referring to is our autofill component - the only hand-written kotlin there is working around a UniFFI limitation (or probably more accurately, a limitation in our megazord layout) - ie, so similar to your example above. But any additional ergonomic improvements tends to be in our consumer code (eg, in android-components)

Copy link
Member Author

Choose a reason for hiding this comment

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

That's really nice! I wonder if we can teach UniFFI about that?

I filed mozilla/uniffi-rs#1470. 😄

However, as UniFFI means the bindings are quite ergonomic, the app-services team has had an informal decision to keep these hand-wrappers as small as possible - if at all.

nod That makes sense, for all the reasons you mentioned—having a substantial amount of hand-written code in Application Services adds another layer of wrapping that can hide important implementation details. I totally agree with your approach of keeping that layer as small as possible (if at all), and saving the ergonomics for our consumers.

components/merino_client/src/error.rs Show resolved Hide resolved
components/merino_client/src/client.rs Show resolved Hide resolved
Copy link
Member Author

@linabutler linabutler left a comment

Choose a reason for hiding this comment

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

Some things for us to work on!

I'm trying out "conventional comments" for review feedback—what do you think? I'm not sure how I feel about them yet... 😄

components/merino_client/src/client.rs Show resolved Hide resolved
interface MerinoSuggestion {
Adm(MerinoSuggestionDetails details, i64 block_id, string full_keyword, string advertiser, string? impression_url, string? click_url);
TopPicks(MerinoSuggestionDetails details, i64 block_id, boolean is_top_pick);
Other(MerinoSuggestionDetails details, string provider);
Copy link
Member Author

Choose a reason for hiding this comment

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

suggestion (blocking): Let's handle AccuWeather suggestions, too. I think adding them will be a fun exercise that covers updating the UniFFI definition, Serde deserialization, and tests! 🎉

/cc @tiftran

}

#[cfg(test)]
mod tests {
Copy link
Member Author

Choose a reason for hiding this comment

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

suggestion (blocking): Let's add some more tests! Ideas for test cases:

  • Other kinds of suggestions (Top Picks, AccuWeather).
  • Specifying additional settings, like the default providers, client variants.
  • Explicitly specifying providers when calling fetch.
  • Session ID and sequence number rotation.
  • Error responses (non-200 response code, bad JSON in body).

/cc @tiftran

linabutler and others added 4 commits February 7, 2023 06:05
@codecov-commenter
Copy link

codecov-commenter commented Mar 16, 2023

Codecov Report

Patch coverage has no change and project coverage change: -0.11 ⚠️

Comparison is base (c637000) 46.58% compared to head (4107209) 46.48%.

❗ Current head 4107209 differs from pull request most recent head 8d1c699. Consider uploading reports for the commit 8d1c699 to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5346      +/-   ##
==========================================
- Coverage   46.58%   46.48%   -0.11%     
==========================================
  Files         181      170      -11     
  Lines       14614    14285     -329     
==========================================
- Hits         6808     6640     -168     
+ Misses       7806     7645     -161     

see 85 files with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@linabutler
Copy link
Member Author

We decided to go in a different direction, and focus on shipping the "basic" Firefox Suggest experience, instead of the "improved" experience powered by Merino.

The basic experience downloads all suggestions from a Remote Settings collection, and matches suggestions on-device. Adding a Merino backend for the improved Suggest experience to our new suggest component (#5723) is out of scope for now, and we have no concrete plans to ship the work in this PR—so let's close it out.

Thanks, folks!

@linabutler linabutler closed this Jul 21, 2023
@rvandermeulen rvandermeulen deleted the merino_client branch August 30, 2023 21:41
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.

5 participants