mirror of
https://github.com/ramsayleung/rspotify.git
synced 2026-04-25 23:45:52 +03:00
[GH-ISSUE #173] Restructure the authentication process #59
Labels
No labels
Stale
bug
discussion
enhancement
good first issue
good first issue
help wanted
pull-request
question
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/rspotify#59
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @marioortizmanero on GitHub (Dec 28, 2020).
Original GitHub issue: https://github.com/ramsayleung/rspotify/issues/173
Back when I first rewrote the authentication process I did improve a few things I thought were necessary, like only having a single http client per
Spotifyinstance, and being more ergonomic and easy to use.But it still leaves a few things to be desired. For example, an
InvalidAutherror variant is needed for whenever a user calls a method fromSpotifythat isn't available due to the authentication process. If one were to use the Client Credentials Flow, the endpoints with access to the user's data shouldn't be available (saymeorcurrently_playing_track). You need to use the Authentication Code Flow for that. This worked well enough because we only had two different methods: the Client Credentials and the Authentication Code.Now that the PKCE Code Authentication Flow has to be implemented, I think it's time to re-think the architecture we're following and make it more idiomatic. This auth flow can access the same endpoints the regular Code Authentication Flow can, but it requires a different setup.
We mainly need three levels for this problem:
rspotify::http::BaseClient. I'll call itHTTPClientfrom now on.BaseClient, a base Spotify client, with access to endpoints such astracksorsearchthat don't need access to the user's data. This usesHTTPClientto make requests.UserClient, an user authenticated Spotify client, with access to endpoints such asme. This may inherit fromBaseClientand implement the extra endpoints over it, or useHTTPClientdirectly and only have the user-authenticated ones.Rust doesn't really have OOP, but we can work it out with traits. Here's the first solution, where the
UserClientinherits fromBaseClientso that the Code Auth flows can access bothUserClientandBaseClientmethods:And here's another solution with a more horizontal hierarchy.
UserClientonly includes the user authenticated methods because it doesn't inherit fromBaseClient, so the Code Auth flows have to implement both of them. I feel like this is more natural in Rust, and it seems more modular because it doesn't assume that user authenticated flows can access the regular ones.In both of these models the implementations take place in the traits, which only needs access to some
HTTPClientmethods, so the structs would only need to implement at most a couple getters for their internal fields, kinda like howIteratorworks.This is purely theoretical so I'm not sure how easy it is to implement. I'll try to code a rough sketch of the latter model, although I'll be quite busy soon with finals so I'm not sure when that'll be possible.
What do you think? Any more ideas?
@ramsayleung commented on GitHub (Dec 30, 2020):
Yep, but in this case, I think
traitscan something likeinterfacein Java.I personally prefer to the second proposal, since it has horizontal hierarchy than the first proposal.
As for the idea that dispatching different endpoints to different authentication clients based on its authentication requirement and flow, it's a great point. But IMHO it will make the endpoints a little bit difficult to use and become less ergonomic, since the developers have to know that does this endpoint need to authenticate with Code Auth Flow or not?
As for other solutions, I am a bit busy on my job, I would propose my thought later, perhaps on this weekend.
@marioortizmanero commented on GitHub (Dec 31, 2020):
How is it less ergonomic? The issue is that users may try to access methods that shouldn't be available. The difference is that rather than allowing them to access these methods and get an authentication error from Spotify, they'll find aAh ok I did misunderstand the comment. You can usually tell if it needs user authentication in the docs page, or by the name (method not availableerror at compile-time. Not sure if I understood your comment well.user_follow_artistobviously requires access to user data).What I recognize may make it less ergonomic is the fact that the users will have to
use Traitwhen they try to call the methods. But that can be fixed by having arspotify::preludemodule with all the traits in the library. That wayuse rspotify::prelude::*will fix any of these issues, and it's a common pattern in other libraries and evenstd(seestd::io::prelude).@marioortizmanero commented on GitHub (Feb 15, 2021):
I've been experimenting with this a bit, here's how it would look like more or less: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e5192749c928122c465d3aa18ede67ce
@marioortizmanero commented on GitHub (Feb 15, 2021):
And I've made a post on the official Rust subreddit to get some advice regarding Rspotify's architecture, see the discussion here: https://www.reddit.com/r/rust/comments/lkdw6o/designing_a_new_architecture_for_rspotify_based/?
@oldwomanjosiah commented on GitHub (Feb 15, 2021):
I've been tinkering with a library for the Twitch Helix API and have been running into similar issues. Would be very interested to see what you come up with.
@marioortizmanero commented on GitHub (Feb 16, 2021):
Note: this will be modified after https://github.com/ramsayleung/rspotify/issues/173 is merged, since the HTTP request methods are no longer implemented over the Spotify client. Instead, the HTTP client is a different struct to keep the logic separate and the abstraction over multiple http libraries clean.
@marioortizmanero commented on GitHub (Feb 20, 2021):
Here's a draft of how it would look like with the proposed changes https://github.com/marioortizmanero/rspotify-architecture, can you take a look before we modify the client anymore, @ramsayleung? Thanks!
The final architecture is still based on the trait system, with a few tweaks to incude
HTTPClientas a member instead of "inheriting" from it. It can get repetitive at times (implementingBaseClientandOAuthClientand similars), so i could use a very simple macro to reduce that. But even then it's not that much boilerplate compared to how much flexibility it offers.I'd like to get this done after #161 to avoid possible conflicts.
@ramsayleung commented on GitHub (Feb 24, 2021):
The full trait inheritance is a little bit complex:
Let's take a look at the simplied one:
The new architecture is much clear than the current one, since the endpoint flow and auth flow are coupling tightly. There is one question about this architecture proposal, since the
CodeAuthPKCESpotifyandCodeAuthSpotifyneed to implement four traits at once, it seems it's unnecessary. Is it possible to combineBaseEndpointsintoBaseClient, it make sense to combine the unidirectional trait inheritance into single one, right? The the trait inheritance will become:@marioortizmanero commented on GitHub (Feb 24, 2021):
Yes, I asked that in the original comment and you agreed with me that it'd be better to have a horizontal hierarchy:
But it might be unnecessarily complex for now. I don't think there'll be an auth method that will let you access user methods but not the basic ones, so a vertical hierarchy makes sense to me. While we're at it we can merge
OAuthClientandOAuthEndpointsinto a singleOAuthClientas well, which is the same as mergingBaseClientwithBaseEndpoints.Can you create a diagram with the full hierarchy once the architecture is definitive so that we can include it in the docs for newcomers? Do you agree with everything else? Maybe other contributors like @kstep or @martinmake can provide their opinions.
@ramsayleung commented on GitHub (Feb 25, 2021):
It makes sense.
Yep, great idea, it will be a pleasure to do that.
Yeah, your proposal is complete, it seems there is nothing left to discuss.
@marioortizmanero commented on GitHub (Mar 8, 2021):
Last thing to consider before I start working on this small rewrite:
Now that the main client doesn't need the builder pattern for its initialization, should we completely get rid of it? Other structs like
Tokendon't really need builder patterns, we were using them for consistency, really. Instead of:You can just write:
We could get rid of
derive_builderthat way. The last way of initialization withDefault::defaultis a bit less known, to be fair, so that's why I'm asking first -- although we could add an example in the docs.All structs currently using the builder pattern:
Token: not that much usage, could work with the typical struct initialization.OAuth: also commonly used and has 4 fields so anewfunction would look worse.Credentials: only has two fields and is commonly needed, so we could implement anewfunction.The
TokenBuilder::from_cachewould have to beToken::from_cachenow, returningOption<Token>. Same goes forCredentialsBuilder's andOAuthBuilder'sfrom_env.Feel free to deny this proposal as I understand it might not be necessary, I just want to at least mention it before this is implemented, now that it's possible.