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

Use ClientBuilder pattern in SDK FFI #739

Merged
merged 16 commits into from
Jun 29, 2022
Merged

Use ClientBuilder pattern in SDK FFI #739

merged 16 commits into from
Jun 29, 2022

Conversation

Anderas
Copy link
Contributor

@Anderas Anderas commented Jun 7, 2022

Change the SDK-ffi architecture to use builder pattern, where the client creates and configures ClientBuilder before creating a client. Further operations, such as login are then methods on the client rather than top-level functions. This is done to closely follow the architecture of the SDK and only provide convenience wrappers for hidden types, concurrency and other iOS-incompatible features.

Additionally expose homeserver_url method to configure custom homeserver, such as localhost.

Define new ClientConfig type which can customize parameters of the
client such as homeserver (incl. localhost) or http_proxy.
@Anderas Anderas requested a review from a team June 7, 2022 15:40
let builder = new_client_builder(base_path, session.user_id.to_string())?
.homeserver_url(&homeurl)
.user_id(&session.user_id);
.homeserver_url(&homeurl);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned in another comment user_id was actually setting server name, and that method is mutually exclusive with homeserver_url (as the docs state, only the last will be used). In effect the homeurl is always overriden by user_id_server, and for example localhost cannot be used properly.

Copy link
Contributor

@gnunicorn gnunicorn left a comment

Choose a reason for hiding this comment

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

I understand the notion - this is mainly for allowing developers to work with a local homeserver, right?

But I'd argue this make the API more confusing as we now have an unclear (and un-intuitive) preference of configuration options. Maybe we can make it more explicit by:
a) make the ClientOption optional itself; and
b) call them config_overwrites or the homeserver-param _overwrite_homeserver

To make clear what has precedence and what is optional...

crates/matrix-sdk-ffi/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 67 to 68
let user = Box::<UserId>::try_from(username.clone())?;
let builder = new_client_builder_with_config(base_path, username, config, &user)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

this ... isn't really making sense to me. UserId must contain a homeserver-address, which we then look up on the rust-side by doing the spec-jumps of .well-known. Given that we have to pass both config and username, I'd argue that username should have preference as it is more specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea I can see this is not ideal. Preferring username if both are available would not work for localhost (without the support of https), but an alternative could be to always set the server from username and potentially override by homeserver if passed from the client (as you suggest in the other comment)

crates/matrix-sdk/src/client/builder.rs Outdated Show resolved Hide resolved
@gnunicorn
Copy link
Contributor

this needs style fixes. You probably want to run cargo xtask fixup and commit the changes, @Anderas

@codecov
Copy link

codecov bot commented Jun 7, 2022

Codecov Report

Merging #739 (3f970ad) into main (091fab8) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##             main     #739   +/-   ##
=======================================
  Coverage   77.76%   77.76%           
=======================================
  Files          92       92           
  Lines       13675    13675           
=======================================
  Hits        10635    10635           
  Misses       3040     3040           

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 091fab8...3f970ad. Read the comment docs.

@poljar
Copy link
Contributor

poljar commented Jun 7, 2022

I understand the notion - this is mainly for allowing developers to work with a local homeserver, right?

But I'd argue this make the API more confusing as we now have an unclear (and un-intuitive) preference of configuration options. Maybe we can make it more explicit by: a) make the ClientOption optional itself; and b) call them config_overwrites or the homeserver-param _overwrite_homeserver

To make clear what has precedence and what is optional...

I might be late to the party, but why do we have new_client_builder() instead of a normal constructor? Why aren't we just mimicking the matrix_sdk API 1:1?

@gnunicorn
Copy link
Contributor

@poljar this FFI API predates the client-builder pattern and we never updated it to it ... arguably, we might want to do that though. @Anderas what do you think? exposing ClientBuilder and its functions instead and use that to construct the proper client and doing a login on it?

The FFI does still have the handy tooling to serialize the homeserver into the token, too, for convenience. But we could keep that and have that reconstruct the data for the client-builder.

@Anderas
Copy link
Contributor Author

Anderas commented Jun 9, 2022

@poljar this FFI API predates the client-builder pattern and we never updated it to it ... arguably, we might want to do that though. @Anderas what do you think? exposing ClientBuilder and its functions instead and use that to construct the proper client and doing a login on it?

The FFI does still have the handy tooling to serialize the homeserver into the token, too, for convenience. But we could keep that and have that reconstruct the data for the client-builder.

That's interesting, I'd like to give this a go. Are you suggesting the client always needs to pass ClientBuilder, but can leave most of its fields as None if there are no overwrites, or would the client builder itself be optional?

Regarding the localhost, maybe I am misunderstanding it, but it seems we cannot extract localhost out of the username unless the localhost runs on https and supports .well-known. And so the only option is to override the servername / url taken out of the username, is this correct?

@poljar
Copy link
Contributor

poljar commented Jun 9, 2022

@poljar this FFI API predates the client-builder pattern and we never updated it to it ... arguably, we might want to do that though. @Anderas what do you think? exposing ClientBuilder and its functions instead and use that to construct the proper client and doing a login on it?
The FFI does still have the handy tooling to serialize the homeserver into the token, too, for convenience. But we could keep that and have that reconstruct the data for the client-builder.

That's interesting, I'd like to give this a go. Are you suggesting the client always needs to pass ClientBuilder, but can leave most of its fields as None if there are no overwrites, or would the client builder itself be optional?

The ClientBuilder has a build() method which returns a Client object. Sadly the examples for the ClientBuilder are a bit poor but the function signature of the ClientBuilder::build() method should make things clear:

pub async fn build(self) -> Result<Client, ClientBuildError>

Regarding the localhost, maybe I am misunderstanding it, but it seems we cannot extract localhost out of the username unless the localhost runs on https and supports .well-known. And so the only option is to override the servername / url taken out of the username, is this correct?

Correct, server discovery requires a .well-known to be set up. The server discovery algorithm has been implemented as the spec defines it and I don't think we'll want to add any special handling of localhost. If people want to avoid server discovery, setting the homeserver address manually should always be possible.

@gnunicorn
Copy link
Contributor

After having experimented a bit, we've found that the client-builder doesn't really work for FFI as the pattern requires &mut self and consumption of self, which UniFFI doesn't really have any concept of. Thus, for moving forward now, we've decided to stay on the (optional) config-option, which can generate the proper ClientBuilder and uses that on the rust-side.

@Anderas
Copy link
Contributor Author

Anderas commented Jun 16, 2022

After having experimented a bit, we've found that the client-builder doesn't really work for FFI as the pattern requires &mut self and consumption of self, which UniFFI doesn't really have any concept of. Thus, for moving forward now, we've decided to stay on the (optional) config-option, which can generate the proper ClientBuilder and uses that on the rust-side.

Thanks for this summary, I was just about to write something along these lines

@Anderas Anderas requested review from gnunicorn and jplatte June 16, 2022 10:19
@jplatte
Copy link
Collaborator

jplatte commented Jun 16, 2022

Is our builder not Clone? I would think we could use Arc::make_mut Arc::try_unwrap to have a reasonably efficient builder pattern that clones in the non-happy path.

@gnunicorn
Copy link
Contributor

Is our builder not Clone?

No and there is good reason for it: as it contains the store_config, which may or may not allow to be cloned (for good reason), we can not expect ClientBuilder to be clone either...

@jplatte
Copy link
Collaborator

jplatte commented Jun 16, 2022

I think we can just put it behind an Arc instead of a Box. I can have a closer look tomorrow.

@Anderas Anderas changed the title Custom homeserver when logging in Use ClientBuilder pattern in SDK FFI + allow custom homeserver when logging in Jun 27, 2022
@Anderas Anderas changed the title Use ClientBuilder pattern in SDK FFI + allow custom homeserver when logging in Use ClientBuilder pattern in SDK FFI Jun 27, 2022
@@ -25,63 +24,6 @@ pub use matrix_sdk::ruma::{api::client::account::register, UserId};

pub use self::{backward_stream::*, client::*, messages::*, room::*};

pub fn guest_client(base_path: String, homeurl: String) -> anyhow::Result<Arc<Client>> {
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 guest client login is not used by element-x, only in a unit test of sample project that could be recreated in other ways, so I did not port this method yet. If I missed some other reason why this is important, I can bring it back

@@ -44,6 +44,25 @@ impl Client {
}
}

pub fn login(&self, username: String, password: String) -> anyhow::Result<()> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are the methods that used to exist as top-level functions, now methods on the Client. Internally they pretty much just call the inner SDK methods of the same name

Copy link
Member

Choose a reason for hiding this comment

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

🙌

/// This method is mutually exclusive with
/// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was
/// set last will be used.
pub fn user_id(self, user_id: &UserId) -> Self {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that user_id naming was confusing (sets server, not user), and server_name_from_user_id was too long, it seems we could simply just rely on server_name(user_id.server_name())

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we please not do this in this PR? I'd rather deprecate it for now, not remove it entirely.

.username(username: "@test:domain")
.build()

try client.login(username: "@test:domain", password: "test")
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 the new ClientBuilder API highlights that username now has to be passed twice, which is not ideal. The client builder needs username to determine file store path, and potentially homeserver, if not set explicitely. The client needs username to construct user to log in with.

Potentially the client could retrieve the username directly from the builder, but not sure if api like client.login(password) is very good.

@Anderas
Copy link
Contributor Author

Anderas commented Jun 27, 2022

After further discussions with @jplatte I refactored the FFI layer to follow the same architecture as the inner SDK, which means exposing a configurable ClientBuilder, later used to build a Client. This means that FFI crate and SDK crate follow the same architecture / patterns.

There are some potentially controversal changes in this PR (removing guest login, removing user_id method) so happy to discuss those further. Also I think this PR has implications for @pixlwave 's work here

Co-authored-by: Jonas Platte <jplatte@element.io>
Comment on lines +79 to +83
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this Default impl used for?

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 was a clippy failure here pointing to this rule

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, that's pretty useless in this context but whatever.

/// This method is mutually exclusive with
/// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was
/// set last will be used.
pub fn user_id(self, user_id: &UserId) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we please not do this in this PR? I'd rather deprecate it for now, not remove it entirely.

@jplatte
Copy link
Collaborator

jplatte commented Jun 27, 2022

@poljar @gnunicorn do you want to re-review before this is merged?

Copy link
Contributor

@gnunicorn gnunicorn left a comment

Choose a reason for hiding this comment

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

Nice,

Just some minor remarks


pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.username = Some(username);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why hold an inner builder but still have all parameters twice? Why not just

Suggested change
builder.username = Some(username);
builder.inner = builder.inner.username(username);

For the homeserver, too...

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can't currently read those fields from the inner builder, but they're needed in build. I think there's room for improvement here, but it's not entirely clear how this should be improved. In general I think the FFI layer shouldn't add its own logic like deriving a store path from a base path plus username / homeserver, but doing that immediately would mean extra work for using the FFI compared to before this change, which is also not great.

inner: MatrixClient::builder().user_agent("rust-sdk-ios"),
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Docs should probably say that base path must be set. Which let's me wonder if we shouldn't have that as a parameters on new so it is enforced and never Option that could fail at runtime (when calling build) . Wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think Andy actually wanted to have it as a new parameter, and I wanted to have it as a builder method instead. My reasoning for that was that it's the usual pattern for builders, and a new ClientBuilder("/some/path") invocation wouldn't be quite clear. (and it would be a very easy to catch programmer error when it isn't set)

However, this point only applies to Kotlin since UniFFI's Swift codegen requires callers to re-state argument names (as in new ClientBuilder(base_path: "/some/path")). So I don't insist on keeping it this way.

Copy link
Contributor Author

@Anderas Anderas Jun 29, 2022

Choose a reason for hiding this comment

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

Yea I generally favor API that makes invalid states impossible to represent, which would require passing them to new. This sounds like an example of clash between SDK and client-side consistency. Happy to follow up on this in a new PR.

let builder = unwrap_or_clone_arc(self);

let base_path = builder.base_path.context("Base path was not set")?;
let username = builder
Copy link
Contributor

Choose a reason for hiding this comment

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

This would fail for guest clients... but as this also drops support fir them, I guess that's fine...

@jplatte jplatte merged commit 3c6d159 into main Jun 29, 2022
@jplatte jplatte deleted the andy/homeserver_login branch June 29, 2022 11:13
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