-
Notifications
You must be signed in to change notification settings - Fork 123
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
Fix IDs v4 #244
Fix IDs v4 #244
Conversation
|
cbde27d
to
2b418f9
Compare
Sorry for not reviewing earlier, I took two hours to understand the context and problem came from #203. Just in case I don't get your point, I am going to explain the problem and your solution in my words. The below endpoint takes async fn start_uris_playback<'a, T: PlayableIdType + 'a>(
&self,
uris: impl IntoIterator<Item = &'a Id<T>> + Send + 'a,
device_id: Option<&str>,
offset: Option<crate::model::Offset<T>>,
position_ms: Option<u32>,
) // if we pass the below uris to `start_uris_playback`, the type of `uris` will be deduced to `impl IntoIterator<Item = &'a Id<Episode>>`.
let uris = &[
EpisodeId::from_uri("spotify:episode:7zC6yTmY67f32WOShs0qbb").unwrap(),
EpisodeId::from_uri("spotify:episode:3hCrwz19QlXY0myYh0XgmL").unwrap(),
TrackId::from_uri("spotify:track:0hAMkY2kwdXPPDfQ1e3BmJ").unwrap()
];
// It will fail to compile since `Track` isn't compatible with `Episode`(not the same type) Your solution to fix this issue is that replacing |
Hi Ramsay, Sorry, the original issue is probably too confusing and lengthy to understand now. I went in circles when trying to implement these IDs and had multiple attempts until I fully understood it. So please ignore it, I'll make a quick summary: To fully cover all the endpoints with type-safe IDs, we need to be able to have IDs of types known at runtime. This is important so that we can have a vector of any kind of ID, such as Now that we know the requirements, we need a solution. In master we have Regarding your comment in #241, the thing is that in the first iteration I tried to do this in a different way without dynamic dispatching. I simply added an If you have any more doubts please let me know. |
Because of the requirements of zero-cost abstraction, then PS: I get your point after adding the below comment, as the original
You turned
It sounds like the downcast problem in OOP language(C++, or Java), results in losing information of derived object by casting derived object to base object |
|
||
#[inline] | ||
unsafe fn from_id_unchecked(id: &str) -> Self { | ||
$name(id.to_owned()) |
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 am a little unsure that is it possible to construct a $name
(for example AlbumId
) from String
? It seems there is no such constructor?
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, it's a tuple struct (defined as struct $name(String)
), which you can initialize with $name(my_string)
. It's basically a newtype over String
.
/// IDs or just `Box<dyn Id>` in case the type wasn't known at compile time. | ||
/// This is why this trait includes both [`Self::_type`] and | ||
/// [`Self::_type_static`], and why all of the static methods use `where Self: | ||
/// Sized`. |
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.
Then, after including the Self::_type
and Self::_type_static
for Id
, the compiler can deduce the specific type of Id
?
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.
These methods are just getters for the Id's type just like what Id::_type
was before this PR (which returns a Type
). See the next comment as to why _type
is necessary.
/// | ||
/// The string passed to this method must be made out of alphanumeric | ||
/// numbers only; otherwise undefined behaviour may occur. | ||
unsafe fn from_id_unchecked(id: &str) -> Self |
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.
Sorry for not getting your point, what are the use cases of _type_static
and from_id_unchecked
, are they necessary?
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.
So, the thing is that Id
is now a trait. And just like how the Rspotify clients work, its structs need to provide getters and similars so that the implementations in Id
work for all their types.
-
from_id_unchecked
serves as a constructor. Since the user shouldn't initialize the ID directly (they should usefrom_id
and similars), it's marked as unsafe. But otherwise there would be no way to construct the Id within the trait (how else wouldfrom_id
be implemented, for instance?). Note that we need object safety, so it useswhere Self: Sized
-
_type
serves as a getter for theType
, and so does_type_static
. The main difference is that_type
is a method, and_type_static
is a static function (it doesn't takeself
). This is because there are two different use-cases to get the type: in constructors, which are static, and in regular methods:- In the
from_id
constructor (static function) if we want to get the type of the ID, we need a static function that provides it. We can't use a method that takes&self
because the type is not constructed yet. So we just callSelf::_type_static
. - In the
uri
method if we want to get the type of the ID, we need either a static function or a method. Any of these would actually work, because the type is already constructed. The problem here is that we want to be able to use this method with adyn Id
. Which requires thaturi
is inId
's vtable. So we can't use a static function in this case,uri
must be a method. And in order foruri
to be a method, it can't make calls to static functions. SoSelf::_type_static
won't work, we needself._type
.
You can see yourself how
_type
is necessary by trying to use_type_static
inuri
instead. You'll get a compiler error that explains it:fn uri(&self) -> String { format!("spotify:{}:{}", Self::_type_static(), self.id()) }
results in:
error[E0277]: the size for values of type `Self` cannot be known at compilation time --> rspotify-model/src/idtypes.rs:159:34 | 159 | format!("spotify:{}:{}", Self::_type_static(), self.id()) | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time | note: required by `idtypes::Id::_type_static` --> rspotify-model/src/idtypes.rs:140:5 | 140 | / fn _type_static() -> Type 141 | | where 142 | | Self: Sized; | |____________________^ help: consider further restricting `Self` | 158 | fn uri(&self) -> String where Self: Sized { | ^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0277`. error: could not compile `rspotify-model` due to previous error
which can be fixed with:
fn uri(&self) -> String where Self: Sized { format!("spotify:{}:{}", Self::_type_static(), self.id()) }
But that won't work as we want it to because
uri
isn't in the vtable:error: the `uri` method cannot be invoked on a trait object --> src/clients/oauth.rs:258:54 | 258 | let uris = track_ids.into_iter().map(|id| id.uri()).collect::<Vec<_>>(); | ^^^ | ::: /home/mario/Programming/rspotify/rspotify-model/src/idtypes.rs:158:41 | 158 | fn uri(&self) -> String where Self: Sized { | ----- this has a `Sized` requirement error: the `uri` method cannot be invoked on a trait object --> src/clients/oauth.rs:976:50 | 976 | "uris": uris.into_iter().map(|id| id.uri()).collect::<Vec<_>>(), | ^^^ | ::: /home/mario/Programming/rspotify/rspotify-model/src/idtypes.rs:158:41 | 158 | fn uri(&self) -> String where Self: Sized { | ----- this has a `Sized` requirement error: could not compile `rspotify` due to 2 previous errors
- In the
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 think I (almost) understand your design, these from_id
, from_uri
and from_id_or_uri
functions both are static constructor, all Id
types can call these constructors without warring about vtable
(of course you could make these functions as virtual function, and all Id
types implement them, but it's clumsy).
As for type
and id
methods, it's necessary to make them as virtual function, while they vary between different Id
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 think I (almost) understand your design, these from_id, from_uri and from_id_or_uri functions both are static constructor, all Id types can call these constructors without warring about vtable(of course you could make these functions as virtual function, and all Id types implement them, but it's clumsy).
Exactly. There's no point on making these constructors virtual functions in the vtable because you couldn't use them. dyn Id
requires an already initialized Id
, so a virtual constructor makes no sense AFAIK. What we want is methods like uri
to be virtual so that we can use them in the endpoints when e.g. taking Vec<dyn Id>
and turning it into a vector of URIs.
As for type and id methods, it's necessary to make them as virtual function, while they vary between different Id
Yup.
@@ -34,16 +32,13 @@ pub struct SimplifiedPlaylist { | |||
pub collaborative: bool, | |||
pub external_urls: HashMap<String, String>, | |||
pub href: String, | |||
pub id: String, | |||
pub id: PlaylistId, |
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.
It seems these Id
types have not implemented the customized serialize and deserialize function, is it ok to replace String
with PlaylistId
, could serde
handle the serialization/deserialization of these Id
type properly?
It's same to the other Id
type.
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.
Ooh it's probably wrong now that you mention it, as the deserialization may even accept an uri on an invalid id. I'll take a look later 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 should be fixed after the last commit
pub positions: Vec<u32>, | ||
} | ||
|
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 ok to remove this new
constructor function?
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, I don't get why it was needed in the first place if all the fields are public. You can just construct it with TrackPostitions { id, positions }
just like how it works for every other model.
|
||
// Deserialization may take either an Id or an URI, so its | ||
// implementation has to be done manually. | ||
impl<'de> Deserialize<'de> for $name { |
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 am not sure if it's necessary to implement the serialize
function manually? Otherwise, the serialized output will be different from the original Json
data?
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 thing is that when deserializing we want to be more flexible and accept both IDs and URIs. And when serializing we can only choose one, so IDs make the most sense to me. What do you suggest otherwise?
@@ -253,7 +253,7 @@ pub trait OAuthClient: BaseClient { | |||
async fn playlist_replace_tracks<'a>( | |||
&self, | |||
playlist_id: &PlaylistId, | |||
track_ids: impl IntoIterator<Item = &'a TrackId> + Send + 'a, | |||
track_ids: impl IntoIterator<Item = &'a dyn PlayableId> + Send + 'a, |
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.
Since it's an endpoint to manipulate track, it doesn't need to be dyn PlayableId
. TrackId
is enough, right?
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 just made a commit that leaves this and similar occurrences as a TODO for another PR: 98c299c. This is because most of the playlist endpoints work with both tracks and episodes, and we don't currently take that into account consistently. This change is correct, but there are many similar ones that should be fixed as well. I've left it as a TODO to avoid making big changes so that this can be merged finally.
I think this PR is ready to merge :) |
Great! Thanks for the review :) |
Ha, thanks for your contribution, you are the guy worked hard :) |
Description
I've finally understood what the Rust compiler was trying to tell me. This PR implements type-safe ID wrappers, it is the only way I've managed to do it. Everything is thoroughly explained in
rspotify-model/src/idtypes.rs
, but if you have any questions please do let me know. In summary, I've ended up being able to makeId
an object-safe trait, so that it can be used asdyn Id
. This way, it is possible to take e.g.Vec<dyn PlayableId>
.This closes #203
Motivation and Context
Now that I've implemented it, the question is: are type-safe IDs worth it? There is currently a partial implementation of IDs in the
master
branch in which I'm basing this PR. We have to either finish said implementation, or remove it completely in a different PR.Advantages:
TrackId
as a parameter makes it pretty clear what kind of ID is required, in comparison with&str
, in which case it would have to be specified in the documentation (or by context).Id::uri
,Id::url
,Id::_type
FullTrack
only need anid
field;uri
and_type
can be removed.Disadvantages:
endpoint(id)
andendpoint(&TrackId::from_uri(id)?)
.String
s instead ofstr
, which require an allocation. If this ends up being important in terms of performance, we could look into crates likesmol_str
(which may not allocate until the string is longer than X bytes), or evenCow
. So performance may not be that much of a problem; we can look into it in the future.Id::from_id
won't work anymore. This is only a disadvantage over the current implementation inmaster
, not over not using type-safe IDs at all.Dependencies
Nothing new
Type of change
Please delete options that are not relevant.
Though it is a breaking change, the only breakage over master occurs when
Id::from_id
and similars were used directly. Instead, a concrete type has to be provided now, likeArtistId::from_id
. The other main breakage is in rspotify's model; some fields have been removed and modified so that they use this new feature instead of just strings. Additionally,IdBuf
has been removed, since now it's always owned anyway.How Has This Been Tested?
There are new tests in
rspotify-model/src/idtypes.rs
. Take a look in there. The previous tests still pass.