diff --git a/CHANGELOG.md b/CHANGELOG.md index b1207b13..9789a708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - ([#332](https://github.com/ramsayleung/rspotify/pull/332)) Fix typo in `RestrictionReason` enum values **Breaking changes**: +- ([#305](https://github.com/ramsayleung/rspotify/pull/305)) The `Id` types have been refactored to maximize usability. Instead of focusing on having an object-safe trait and using `dyn Id`, we now have enums to group up the IDs. This is based on how [`enum_dispatch`](https://docs.rs/enum_dispatch) works, and it's not only easier to use, but also more efficient. It makes it possible to have borrowed IDs again, so we've chosen to use `Cow` internally for flexibility. Check out the docs for more information! + + Please let us know if there is anything that could be improved. Unfortunately, this breaks many methods in `BaseClient` and `OAuthClient`, but the errors should occur at compile-time only. - ([#325](https://github.com/ramsayleung/rspotify/pull/325)) The `auth_code`, `auth_code_pkce`, `client_creds`, `clients::base` and `clients::oauth` modules have been removed from the public API; you should access the same types from their parent modules instead - ([#326](https://github.com/ramsayleung/rspotify/pull/326)) The `rspotify::clients::mutex` module has been renamed to `rspotify::sync` - ([#330](https://github.com/ramsayleung/rspotify/pull/330)) `search` now accepts `Option` instead of `Option<&IncludeExternal>` diff --git a/examples/auth_code.rs b/examples/auth_code.rs index 23292561..95f16e40 100644 --- a/examples/auth_code.rs +++ b/examples/auth_code.rs @@ -49,5 +49,5 @@ async fn main() { .current_playing(Some(market), Some(&additional_types)) .await; - println!("Response: {:?}", artists); + println!("Response: {artists:?}"); } diff --git a/examples/auth_code_pkce.rs b/examples/auth_code_pkce.rs index 239469cb..fb3164ef 100644 --- a/examples/auth_code_pkce.rs +++ b/examples/auth_code_pkce.rs @@ -39,7 +39,7 @@ async fn main() { // Running the requests let history = spotify.current_playback(None, None::>).await; - println!("Response: {:?}", history); + println!("Response: {history:?}"); // Token refreshing works as well, but only with the one generated in the // previous request (they actually expire, unlike the regular code auth @@ -51,5 +51,5 @@ async fn main() { // Running the requests again let history = spotify.current_playback(None, None::>).await; - println!("Response after refreshing token: {:?}", history); + println!("Response after refreshing token: {history:?}"); } diff --git a/examples/client_creds.rs b/examples/client_creds.rs index 22aee06e..e91069a3 100644 --- a/examples/client_creds.rs +++ b/examples/client_creds.rs @@ -33,7 +33,7 @@ async fn main() { // Running the requests let birdy_uri = AlbumId::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); - let albums = spotify.album(&birdy_uri).await; + let albums = spotify.album(birdy_uri).await; - println!("Response: {:#?}", albums); + println!("Response: {albums:#?}"); } diff --git a/examples/tasks.rs b/examples/tasks.rs index cc03880b..53dda232 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -28,7 +28,7 @@ async fn main() { let spotify = Arc::clone(&spotify); let wr = wr.clone(); let handle = task::spawn(async move { - let albums = spotify.album(&id).await.unwrap(); + let albums = spotify.album(id).await.unwrap(); wr.send(albums).unwrap(); }); diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index 9b0192cf..2d9f89b1 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -18,5 +18,5 @@ fn main() { let devices = spotify.device(); - println!("Request: {:?}", devices); + println!("Request: {devices:?}"); } diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index dc508bc5..08dc4355 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -17,5 +17,5 @@ fn main() { spotify.prompt_for_token(&url).unwrap(); let user = spotify.me(); - println!("Request: {:?}", user); + println!("Request: {user:?}"); } diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index d6c4da26..169e43c5 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -19,8 +19,8 @@ fn main() { let album_query = "album:arrival artist:abba"; let result = spotify.search(album_query, SearchType::Album, None, None, Some(10), None); match result { - Ok(album) => println!("searched album:{:?}", album), - Err(err) => println!("search error!{:?}", err), + Ok(album) => println!("Searched album: {album:?}"), + Err(err) => println!("Search error! {err:?}"), } let artist_query = "tania bowra"; @@ -33,8 +33,8 @@ fn main() { None, ); match result { - Ok(album) => println!("searched artist:{:?}", album), - Err(err) => println!("search error!{:?}", err), + Ok(album) => println!("Searched artist: {album:?}"), + Err(err) => println!("Search error! {err:?}"), } let playlist_query = "\"doom metal\""; @@ -47,8 +47,8 @@ fn main() { None, ); match result { - Ok(album) => println!("searched playlist:{:?}", album), - Err(err) => println!("search error!{:?}", err), + Ok(album) => println!("Searched playlist: {album:?}"), + Err(err) => println!("Search error! {err:?}"), } let track_query = "abba"; @@ -61,15 +61,15 @@ fn main() { None, ); match result { - Ok(album) => println!("searched track:{:?}", album), - Err(err) => println!("search error!{:?}", err), + Ok(album) => println!("Searched track: {album:?}"), + Err(err) => println!("Search error! {err:?}"), } let show_query = "love"; let result = spotify.search(show_query, SearchType::Show, None, None, Some(10), None); match result { - Ok(show) => println!("searched show:{:?}", show), - Err(err) => println!("search error!{:?}", err), + Ok(show) => println!("Searched show: {show:?}"), + Err(err) => println!("Search error! {err:?}"), } let episode_query = "love"; @@ -82,7 +82,7 @@ fn main() { None, ); match result { - Ok(episode) => println!("searched episode:{:?}", episode), - Err(err) => println!("search error!{:?}", err), + Ok(episode) => println!("Searched episode: {episode:?}"), + Err(err) => println!("Search error! {err:?}"), } } diff --git a/examples/ureq/seek_track.rs b/examples/ureq/seek_track.rs index 11763790..b4907954 100644 --- a/examples/ureq/seek_track.rs +++ b/examples/ureq/seek_track.rs @@ -17,7 +17,7 @@ fn main() { spotify.prompt_for_token(&url).unwrap(); match spotify.seek_track(25000, None) { - Ok(_) => println!("change to previous playback successful"), - Err(_) => eprintln!("change to previous playback failed"), + Ok(_) => println!("Change to previous playback successful"), + Err(_) => eprintln!("Change to previous playback failed"), } } diff --git a/examples/ureq/threading.rs b/examples/ureq/threading.rs index 6d3f544b..1abf39ee 100644 --- a/examples/ureq/threading.rs +++ b/examples/ureq/threading.rs @@ -29,7 +29,7 @@ fn main() { let spotify = Arc::clone(&spotify); let wr = wr.clone(); let handle = thread::spawn(move || { - let albums = spotify.album(&id).unwrap(); + let albums = spotify.album(id).unwrap(); wr.send(albums).unwrap(); }); diff --git a/examples/webapp/Cargo.toml b/examples/webapp/Cargo.toml index 9802eea7..4fe19d24 100644 --- a/examples/webapp/Cargo.toml +++ b/examples/webapp/Cargo.toml @@ -1,12 +1,15 @@ [package] name = "webapp" version = "0.1.0" -authors = ["Ramsay "] +authors = [ + "Ramsay Leung ", + "Mario Ortiz Manero " +] edition = "2018" [dependencies] -rocket = "0.4.5" -rocket_contrib = { version = "0.4.5", features = ["tera_templates"] } -getrandom = "0.2.0" +rocket = "0.4.10" +rocket_contrib = { version = "0.4.10", features = ["tera_templates"] } +getrandom = "0.2.6" # Rocket is synchronous, so this uses the `ureq` client rspotify = { path = "../..", features = ["client-ureq", "ureq-rustls-tls"], default-features = false } diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index e2bf55df..b94d3e40 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -14,13 +14,13 @@ use rocket::response::Redirect; use rocket_contrib::json; use rocket_contrib::json::JsonValue; use rocket_contrib::templates::Template; -use rspotify::{scopes, AuthCodeSpotify, OAuth, Credentials, Config, prelude::*, Token}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Config, Credentials, OAuth, Token}; -use std::fs; use std::{ collections::HashMap, - env, + env, fs, path::PathBuf, + sync::{Arc, Mutex}, }; #[derive(Debug, Responder)] @@ -95,7 +95,7 @@ fn init_spotify(cookies: &Cookies) -> AuthCodeSpotify { // Replacing client_id and client_secret with yours. let creds = Credentials::new( "e1dce60f1e274e20861ce5d96142a4d3", - "0e4e03b9be8d465d87fc32857a4b5aa3" + "0e4e03b9be8d465d87fc32857a4b5aa3", ); AuthCodeSpotify::with_config(creds, oauth, config) @@ -149,7 +149,7 @@ fn index(mut cookies: Cookies) -> AppResponse { AppResponse::Template(Template::render("index", context.clone())) } Err(err) => { - context.insert("err_msg", format!("Failed for {}!", err)); + context.insert("err_msg", format!("Failed for {err}!")); AppResponse::Template(Template::render("error", context)) } } @@ -168,9 +168,10 @@ fn playlist(cookies: Cookies) -> AppResponse { return AppResponse::Redirect(Redirect::to("/")); } - let token = spotify.read_token_cache().unwrap(); - spotify.token = Some(token); - let playlists = spotify.current_user_playlists() + let token = spotify.read_token_cache(false).unwrap(); + spotify.token = Arc::new(Mutex::new(token)); + let playlists = spotify + .current_user_playlists() .take(50) .filter_map(Result::ok) .collect::>(); @@ -189,7 +190,7 @@ fn me(cookies: Cookies) -> AppResponse { return AppResponse::Redirect(Redirect::to("/")); } - spotify.token = Some(spotify.read_token_cache().unwrap()); + spotify.token = Arc::new(Mutex::new(spotify.read_token_cache(false).unwrap())); match spotify.me() { Ok(user_info) => AppResponse::Json(json!(user_info)), Err(_) => AppResponse::Redirect(Redirect::to("/")), diff --git a/examples/with_auto_reauth.rs b/examples/with_auto_reauth.rs index 57e3b80e..95ecc6ea 100644 --- a/examples/with_auto_reauth.rs +++ b/examples/with_auto_reauth.rs @@ -14,15 +14,16 @@ use rspotify::{ // followed artists, and then unfollow the artists. async fn auth_code_do_things(spotify: &AuthCodeSpotify) { let artists = [ - &ArtistId::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash - &ArtistId::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order - &ArtistId::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha + ArtistId::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash + ArtistId::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order + ArtistId::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha ]; + let num_artists = artists.len(); spotify - .user_follow_artists(artists) + .user_follow_artists(artists.iter().map(|a| a.as_ref())) .await .expect("couldn't follow artists"); - println!("Followed {} artists successfully.", artists.len()); + println!("Followed {num_artists} artists successfully."); // Printing the followed artists let followed = spotify @@ -38,13 +39,13 @@ async fn auth_code_do_things(spotify: &AuthCodeSpotify) { .user_unfollow_artists(artists) .await .expect("couldn't unfollow artists"); - println!("Unfollowed {} artists successfully.", artists.len()); + println!("Unfollowed {num_artists} artists successfully."); } async fn client_creds_do_things(spotify: &ClientCredsSpotify) { // Running the requests let birdy_uri = AlbumId::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); - let albums = spotify.album(&birdy_uri).await; + let albums = spotify.album(birdy_uri).await; println!("Get albums: {}", albums.unwrap().id); } diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 54063df6..f2f3ef13 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -21,15 +21,16 @@ use rspotify::{model::ArtistId, prelude::*, scopes, AuthCodeSpotify, Credentials // followed artists, and then unfollow the artists. async fn do_things(spotify: AuthCodeSpotify) { let artists = [ - &ArtistId::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash - &ArtistId::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order - &ArtistId::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha + ArtistId::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash + ArtistId::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order + ArtistId::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha ]; + let num_artists = artists.len(); spotify - .user_follow_artists(artists) + .user_follow_artists(artists.iter().map(|a| a.as_ref())) .await .expect("couldn't follow artists"); - println!("Followed {} artists successfully.", artists.len()); + println!("Followed {num_artists} artists successfully."); // Printing the followed artists let followed = spotify @@ -45,7 +46,7 @@ async fn do_things(spotify: AuthCodeSpotify) { .user_unfollow_artists(artists) .await .expect("couldn't unfollow artists"); - println!("Unfollowed {} artists successfully.", artists.len()); + println!("Unfollowed {num_artists} artists successfully."); } #[tokio::main] diff --git a/rspotify-model/Cargo.toml b/rspotify-model/Cargo.toml index fa60194d..7af6ef27 100644 --- a/rspotify-model/Cargo.toml +++ b/rspotify-model/Cargo.toml @@ -15,8 +15,8 @@ readme = "../README.md" [dependencies] chrono = { version = "0.4.19", features = ["serde", "rustc-serialize"] } +enum_dispatch = "0.3.8" serde = { version = "1.0.130", features = ["derive"] } serde_json = "1.0.67" strum = { version = "0.24.0", features = ["derive"] } thiserror = "1.0.29" - diff --git a/rspotify-model/src/album.rs b/rspotify-model/src/album.rs index 374ef819..f91f641d 100644 --- a/rspotify-model/src/album.rs +++ b/rspotify-model/src/album.rs @@ -21,7 +21,7 @@ pub struct SimplifiedAlbum { pub available_markets: Vec, pub external_urls: HashMap, pub href: Option, - pub id: Option, + pub id: Option>, pub images: Vec, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -43,7 +43,7 @@ pub struct FullAlbum { pub external_urls: HashMap, pub genres: Vec, pub href: String, - pub id: AlbumId, + pub id: AlbumId<'static>, pub images: Vec, pub name: String, pub popularity: u32, diff --git a/rspotify-model/src/artist.rs b/rspotify-model/src/artist.rs index 42d11e4b..2906913e 100644 --- a/rspotify-model/src/artist.rs +++ b/rspotify-model/src/artist.rs @@ -11,7 +11,7 @@ use crate::{ArtistId, CursorBasedPage, Followers, Image}; pub struct SimplifiedArtist { pub external_urls: HashMap, pub href: Option, - pub id: Option, + pub id: Option>, pub name: String, } @@ -22,7 +22,7 @@ pub struct FullArtist { pub followers: Followers, pub genres: Vec, pub href: String, - pub id: ArtistId, + pub id: ArtistId<'static>, pub images: Vec, pub name: String, pub popularity: u32, diff --git a/rspotify-model/src/audio.rs b/rspotify-model/src/audio.rs index 169fc54e..b376ac9c 100644 --- a/rspotify-model/src/audio.rs +++ b/rspotify-model/src/audio.rs @@ -18,7 +18,7 @@ pub struct AudioFeatures { #[serde(with = "duration_ms", rename = "duration_ms")] pub duration: Duration, pub energy: f32, - pub id: TrackId, + pub id: TrackId<'static>, pub instrumentalness: f32, pub key: i32, pub liveness: f32, diff --git a/rspotify-model/src/idtypes.rs b/rspotify-model/src/idtypes.rs index 62f81e7f..b44c320a 100644 --- a/rspotify-model/src/idtypes.rs +++ b/rspotify-model/src/idtypes.rs @@ -1,11 +1,12 @@ -//! This module defines the necessary elements in order to represent Spotify IDs -//! and URIs with type safety and no overhead. +//! This module makes it possible to represent Spotify IDs and URIs with type +//! safety and almost no overhead. //! //! ## Concrete IDs //! //! The trait [`Id`] is the central element of this module. It's implemented by -//! different kinds of ID ([`AlbumId`], [`EpisodeId`], etc), and implements the -//! logic to initialize and use IDs. +//! all kinds of ID, and includes the main functionality to use them. Remember +//! that you will need to import this trait to access its methods. The easiest +//! way is to add `use rspotify::prelude::*`. //! //! * [`Type::Artist`] => [`ArtistId`] //! * [`Type::Album`] => [`AlbumId`] @@ -15,143 +16,125 @@ //! * [`Type::Show`] => [`ShowId`] //! * [`Type::Episode`] => [`EpisodeId`] //! +//! Every kind of ID defines its own validity function, i.e., what characters it +//! can be made up of, such as alphanumeric or any. +//! +//! These types are just wrappers for [`Cow`], so their usage should be +//! quite similar overall. +//! +//! [`Cow`]: [`std::borrow::Cow`] +//! //! ## Examples //! //! If an endpoint requires a `TrackId`, you may pass it as: //! //! ``` -//! use rspotify_model::{Id, TrackId}; -//! -//! fn pause_track(id: &TrackId) { /* ... */ } +//! # use rspotify_model::TrackId; +//! fn pause_track(id: TrackId<'_>) { /* ... */ } //! //! let id = TrackId::from_id("4iV5W9uYEdYUVa79Axb7Rh").unwrap(); -//! pause_track(&id); +//! pause_track(id); //! ``` //! //! Notice how this way it's type safe; the following example would fail at //! compile-time: //! //! ```compile_fail -//! use rspotify_model::{Id, TrackId, EpisodeId}; -//! -//! fn pause_track(id: &TrackId) { /* ... */ } +//! # use rspotify_model::{TrackId, EpisodeId}; +//! fn pause_track(id: TrackId<'_>) { /* ... */ } //! //! let id = EpisodeId::from_id("4iV5W9uYEdYUVa79Axb7Rh").unwrap(); -//! pause_track(&id); +//! pause_track(id); //! ``` //! //! And this would panic because it's a `TrackId` but its URI string specifies //! it's an album (`spotify:album:xxxx`). //! //! ```should_panic -//! use rspotify_model::{Id, TrackId}; -//! -//! fn pause_track(id: &TrackId) { /* ... */ } +//! # use rspotify_model::TrackId; +//! fn pause_track(id: TrackId<'_>) { /* ... */ } //! //! let id = TrackId::from_uri("spotify:album:6akEvsycLGftJxYudPjmqK").unwrap(); -//! pause_track(&id); +//! pause_track(id); //! ``` //! //! A more complex example where an endpoint takes a vector of IDs of different //! types: //! //! ``` -//! use rspotify_model::{Id, TrackId, EpisodeId, PlayableId}; +//! use rspotify_model::{TrackId, EpisodeId, PlayableId}; //! -//! fn track(id: &TrackId) { /* ... */ } -//! fn episode(id: &EpisodeId) { /* ... */ } -//! fn add_to_queue(id: &[&dyn PlayableId]) { /* ... */ } +//! fn track(id: TrackId<'_>) { /* ... */ } +//! fn episode(id: EpisodeId<'_>) { /* ... */ } +//! fn add_to_queue(id: &[PlayableId<'_>]) { /* ... */ } //! -//! let tracks = &[ +//! let tracks = [ //! TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), //! TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap(), //! ]; -//! let episodes = &[ +//! let episodes = [ //! EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), //! EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap(), //! ]; //! //! // First we get some info about the tracks and episodes -//! let track_info = tracks.iter().map(|id| track(id)).collect::>(); -//! let ep_info = episodes.iter().map(|id| episode(id)).collect::>(); +//! let track_info = tracks.iter().map(|id| track(id.as_ref())).collect::>(); +//! let ep_info = episodes.iter().map(|id| episode(id.as_ref())).collect::>(); //! println!("Track info: {:?}", track_info); //! println!("Episode info: {:?}", ep_info); //! //! // And then we add both the tracks and episodes to the queue //! let playable = tracks -//! .iter() -//! .map(|id| id as &dyn PlayableId) +//! .into_iter() +//! .map(|t| t.as_ref().into()) //! .chain( -//! episodes.iter().map(|id| id as &dyn PlayableId) +//! episodes.into_iter().map(|e| e.as_ref().into()) //! ) -//! .collect::>(); +//! .collect::>(); //! add_to_queue(&playable); //! ``` +use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; use strum::Display; use thiserror::Error; -use std::fmt::Debug; -use std::hash::Hash; +use std::{borrow::Cow, fmt::Debug, hash::Hash}; use crate::Type; -/// Spotify id or URI parsing error +/// Spotify ID or URI parsing error /// /// See also [`Id`](crate::idtypes::Id) for details. #[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] pub enum IdError { - /// Spotify URI prefix is not `spotify:` or `spotify/` + /// Spotify URI prefix is not `spotify:` or `spotify/`. InvalidPrefix, - /// Spotify URI can't be split into type and id parts - /// (e.g. it has invalid separator) + /// Spotify URI can't be split into type and id parts (e.g., it has invalid + /// separator). InvalidFormat, /// Spotify URI has invalid type name, or id has invalid type in a given - /// context (e.g. a method expects a track id, but artist id is provided) + /// context (e.g. a method expects a track id, but artist id is provided). InvalidType, - /// Spotify id is invalid (empty or contains invalid characters) + /// Spotify id is invalid (empty or contains invalid characters). InvalidId, } /// The main interface for an ID. /// -/// # Implementation note +/// See the [module level documentation] for more information. /// -/// Note that for IDs to be useful their trait must be object-safe. Otherwise, -/// it wouldn't be possible to use `Vec>` to take multiple kinds of -/// IDs or just `Box` 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`. -/// -/// Unfortunately, since `where Self: Sized` has to be enforced, IDs cannot be -/// simply a `str` internally because these aren't sized. Thus, IDs will have the -/// slight overhead of having to use an owned type like `String`. -pub trait Id: Send + Sync { - /// Spotify object Id (guaranteed to be valid for that type) +/// [module level documentation]: [`crate::idtypes`] +#[enum_dispatch] +pub trait Id { + /// Returns the inner Spotify object ID, which is guaranteed to be valid for + /// its type. fn id(&self) -> &str; - /// The type of the ID. The difference with [`Self::_type_static`] is that - /// this method can be used so that `Id` is an object-safe trait. + /// The type of the ID, as a function. fn _type(&self) -> Type; - /// The type of the ID, which can be used without initializing it - fn _type_static() -> Type - where - Self: Sized; - - /// Initialize the Id without checking its validity. - /// - /// # Safety - /// - /// The string passed to this method must be made out of valid characters - /// only; otherwise undefined behaviour may occur. - unsafe fn from_id_unchecked(id: &str) -> Self - where - Self: Sized; - - /// Spotify object URI in a well-known format: `spotify:type:id` + /// Returns a Spotify object URI in a well-known format: `spotify:type:id`. /// /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, /// `spotify:track:4y4VO05kYgUTo2bzbox1an`. @@ -159,179 +142,236 @@ pub trait Id: Send + Sync { format!("spotify:{}:{}", self._type(), self.id()) } - /// Full Spotify object URL, can be opened in a browser + /// Returns a full Spotify object URL that can be opened in a browser. /// /// Examples: `https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an`, /// `https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI`. fn url(&self) -> String { format!("https://open.spotify.com/{}/{}", self._type(), self.id()) } +} - /// Parse Spotify id from string slice - /// - /// A valid Spotify object id must be a non-empty string with valid - /// characters. - /// - /// # Errors: - /// - /// - `IdError::InvalidId` - if `id` contains invalid characters. - fn from_id(id: &str) -> Result - where - Self: Sized, - { - if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { - // Safe, we've just checked that the Id is valid. - Ok(unsafe { Self::from_id_unchecked(id) }) - } else { - Err(IdError::InvalidId) - } - } - - /// Parse Spotify URI from string slice - /// - /// Spotify URI must be in one of the following formats: - /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. - /// Where `{type}` is one of `artist`, `album`, `track`, `playlist`, `user`, - /// `show`, or `episode`, and `{id}` is a non-empty valid string. - /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, - /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. - /// - /// # Errors: - /// - /// - `IdError::InvalidPrefix` - if `uri` is not started with `spotify:` - /// or `spotify/`, - /// - `IdError::InvalidType` - if type part of an `uri` is not a valid - /// Spotify type `T`, - /// - `IdError::InvalidId` - if id part of an `uri` is not a valid id, - /// - `IdError::InvalidFormat` - if it can't be splitted into type and - /// id parts. - fn from_uri(uri: &str) -> Result - where - Self: Sized, - { - let mut chars = uri - .strip_prefix("spotify") - .ok_or(IdError::InvalidPrefix)? - .chars(); - let sep = match chars.next() { - Some(ch) if ch == '/' || ch == ':' => ch, - _ => return Err(IdError::InvalidPrefix), - }; - let rest = chars.as_str(); - - let (tpe, id) = rest - .rfind(sep) - .map(|mid| rest.split_at(mid)) - .ok_or(IdError::InvalidFormat)?; - - // Note that in case the type isn't known at compile time, any type will - // be accepted. - match tpe.parse::() { - Ok(tpe) if tpe == Self::_type_static() => Self::from_id(&id[1..]), - _ => Err(IdError::InvalidType), - } - } - - /// Parse Spotify id or URI from string slice - /// - /// Spotify URI must be in one of the following formats: - /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. - /// Where `{type}` is one of `artist`, `album`, `track`, `playlist`, `user`, - /// `show`, or `episode`, and `{id}` is a non-empty valid string. The URI - /// must be match with the ID's type (`Id::TYPE`), otherwise - /// `IdError::InvalidType` error is returned. - /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, - /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. - /// - /// If input string is not a valid Spotify URI (it's not started with - /// `spotify:` or `spotify/`), it must be a valid Spotify object ID, - /// i.e. a non-empty valid string. - /// - /// # Errors: - /// - /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part - /// is not equal to `T`, - /// - `IdError::InvalidId` - either if `id_or_uri` is an URI with invalid id - /// part, or it's an invalid id (id is invalid if it contains valid - /// characters), - /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be - /// split into type and id parts. - fn from_id_or_uri(id_or_uri: &str) -> Result - where - Self: Sized, - { - match Self::from_uri(id_or_uri) { - Ok(id) => Ok(id), - Err(IdError::InvalidPrefix) => Self::from_id(id_or_uri), - Err(error) => Err(error), - } +/// A lower level function to parse a URI into both its type and its actual ID. +/// Note that this function doesn't check the validity of the returned ID (e.g., +/// whether it's alphanumeric; that should be done in `Id::from_id`). +/// +/// This is only useful for advanced use-cases, such as implementing your own ID +/// type. +pub fn parse_uri(uri: &str) -> Result<(Type, &str), IdError> { + let mut chars = uri + .strip_prefix("spotify") + .ok_or(IdError::InvalidPrefix)? + .chars(); + let sep = match chars.next() { + Some(ch) if ch == '/' || ch == ':' => ch, + _ => return Err(IdError::InvalidPrefix), + }; + let rest = chars.as_str(); + + let (tpe, id) = rest + .rfind(sep) + .map(|mid| rest.split_at(mid)) + .ok_or(IdError::InvalidFormat)?; + + // Note that in case the type isn't known at compile time, + // any type will be accepted. + match tpe.parse::() { + Ok(tpe) => Ok((tpe, &id[1..])), + _ => Err(IdError::InvalidType), } } -pub trait PlayableId: Id {} -pub trait PlayContextId: Id {} - /// This macro helps consistently define ID types. /// -/// * The `$type` parameter indicates what type the ID is made out of (say, -/// `Artist`, `Album`...) from the enum `Type`. -/// * The `$name` parameter is the identifier of the struct for that type. +/// * The `$type` parameter indicates what variant in `Type` the ID is for (say, +/// `Artist`, or `Album`). +/// * The `$name` parameter is the identifier of the struct. +/// * The `$validity` parameter is the implementation of `id_is_valid`. macro_rules! define_idtypes { - ($($type:ident => $name:ident),+) => { + ($($type:ident => { + name: $name:ident, + validity: $validity:expr + }),+) => { $( #[doc = concat!( - "ID of type [`Type::", stringify!($type), "`], made up of only \ - alphanumeric characters. Refer to the [module-level \ - docs][`crate::idtypes`] for more information.") - ] + "ID of type [`Type::", stringify!($type), "`]. The validity of \ + its characters is defined by the closure `", + stringify!($validity), "`.\n\nRefer to the [module-level \ + docs][`crate::idtypes`] for more information. " + )] + #[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] - pub struct $name(String); + pub struct $name<'a>(Cow<'a, str>); + + impl<'a> $name<'a> { + /// The type of the ID, as a constant. + const TYPE: Type = Type::$type; + + /// Only returns `true` in case the given string is valid + /// according to that specific ID (e.g., some may require + /// alphanumeric characters only). + pub fn id_is_valid(id: &str) -> bool { + const VALID_FN: fn(&str) -> bool = $validity; + VALID_FN(id) + } - impl Id for $name { - #[inline] - fn id(&self) -> &str { - &self.0 + /// Initialize the ID without checking its validity. + /// + /// # Safety + /// + /// The string passed to this method must be made out of valid + /// characters only; otherwise undefined behaviour may occur. + pub unsafe fn from_id_unchecked(id: S) -> Self + where + S: Into> + { + Self(id.into()) } - #[inline] - fn _type(&self) -> Type { - Type::$type + /// Parse Spotify ID from string slice. + /// + /// A valid Spotify object id must be a non-empty string with + /// valid characters. + /// + /// # Errors + /// + /// - `IdError::InvalidId` - if `id` contains invalid characters. + pub fn from_id(id: S) -> Result + where + S: Into> + { + let id = id.into(); + if Self::id_is_valid(&id) { + // Safe, we've just checked that the ID is valid. + Ok(unsafe { Self::from_id_unchecked(id) }) + } else { + Err(IdError::InvalidId) + } } - #[inline] - fn _type_static() -> Type where Self: Sized { - Type::$type + /// Parse Spotify URI from string slice + /// + /// Spotify URI must be in one of the following formats: + /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. + /// Where `{type}` is one of `artist`, `album`, `track`, + /// `playlist`, `user`, `show`, or `episode`, and `{id}` is a + /// non-empty valid string. + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// + /// # Errors + /// + /// - `IdError::InvalidPrefix` - if `uri` is not started with + /// `spotify:` or `spotify/`, + /// - `IdError::InvalidType` - if type part of an `uri` is not a + /// valid Spotify type `T`, + /// - `IdError::InvalidId` - if id part of an `uri` is not a + /// valid id, + /// - `IdError::InvalidFormat` - if it can't be splitted into + /// type and id parts. + /// + /// # Implementation details + /// + /// Unlike [`Self::from_id`], this method takes a `&str` rather + /// than an `Into>`. This is because the inner `Cow` in + /// the ID would reference a slice from the given `&str` (i.e., + /// taking the ID out of the URI). The parameter wouldn't live + /// long enough when using `Into>`, so the only + /// sensible choice is to just use a `&str`. + pub fn from_uri(uri: &'a str) -> Result { + let (tpe, id) = parse_uri(&uri)?; + if tpe == Type::$type { + Self::from_id(id) + } else { + Err(IdError::InvalidType) + } } - #[inline] - unsafe fn from_id_unchecked(id: &str) -> Self { - $name(id.to_owned()) + /// Parse Spotify ID or URI from string slice + /// + /// Spotify URI must be in one of the following formats: + /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. + /// Where `{type}` is one of `artist`, `album`, `track`, + /// `playlist`, `user`, `show`, or `episode`, and `{id}` is a + /// non-empty valid string. The URI must be match with the ID's + /// type (`Id::TYPE`), otherwise `IdError::InvalidType` error is + /// returned. + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// + /// If input string is not a valid Spotify URI (it's not started + /// with `spotify:` or `spotify/`), it must be a valid Spotify + /// object ID, i.e. a non-empty valid string. + /// + /// # Errors + /// + /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and + /// it's type part is not equal to `T`, + /// - `IdError::InvalidId` - either if `id_or_uri` is an URI + /// with invalid id part, or it's an invalid id (id is invalid + /// if it contains valid characters), + /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and + /// it can't be split into type and id parts. + /// + /// # Implementation details + /// + /// Unlike [`Self::from_id`], this method takes a `&str` rather + /// than an `Into>`. This is because the inner `Cow` in + /// the ID would reference a slice from the given `&str` (i.e., + /// taking the ID out of the URI). The parameter wouldn't live + /// long enough when using `Into>`, so the only + /// sensible choice is to just use a `&str`. + pub fn from_id_or_uri(id_or_uri: &'a str) -> Result { + match Self::from_uri(id_or_uri) { + Ok(id) => Ok(id), + Err(IdError::InvalidPrefix) => Self::from_id(id_or_uri), + Err(error) => Err(error), + } + } + + /// This creates an ID with the underlying `&str` variant from a + /// reference. Useful to use an ID multiple times without having + /// to clone it. + pub fn as_ref(&'a self) -> Self { + Self(Cow::Borrowed(self.0.as_ref())) + } + + /// An ID is a `Cow` after all, so this will switch to the its + /// owned version, which has a `'static` lifetime. + pub fn into_static(self) -> $name<'static> { + $name(Cow::Owned(self.0.into_owned())) + } + + /// Similar to [`Self::into_static`], but without consuming the + /// original ID. + pub fn clone_static(&self) -> $name<'static> { + $name(Cow::Owned(self.0.clone().into_owned())) } } - )+ - } -} -/// This macro contains a lot of code but mostly it's just repetitive work to -/// implement some common traits that's not of much interest for being trivial. -/// -/// * The `$name` parameter is the identifier of the struct for that type. -macro_rules! define_impls { - ($($name:ident),+) => { - $( - // Deserialization may take either an Id or an URI, so its + impl Id for $name<'_> { + fn id(&self) -> &str { + &self.0 + } + + fn _type(&self) -> Type { + Self::TYPE + } + } + + // Deserialization may take either an ID or an URI, so its // implementation has to be done manually. - impl<'de> Deserialize<'de> for $name { - fn deserialize(deserializer: D) -> Result<$name, D::Error> + impl<'de> Deserialize<'de> for $name<'static> { + fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct IdVisitor; impl<'de> serde::de::Visitor<'de> for IdVisitor { - type Value = $name; + type Value = $name<'static>; fn expecting( &self, formatter: &mut std::fmt::Formatter<'_> @@ -341,16 +381,15 @@ macro_rules! define_impls { formatter.write_str(msg) } - #[inline] fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { $name::from_id_or_uri(value) - .map_err(serde::de::Error::custom) + .map($name::into_static) + .map_err(serde::de::Error::custom) } - #[inline] fn visit_newtype_struct( self, deserializer: A, @@ -361,7 +400,6 @@ macro_rules! define_impls { deserializer.deserialize_str(self) } - #[inline] fn visit_seq( self, mut seq: A, @@ -369,9 +407,10 @@ macro_rules! define_impls { where A: serde::de::SeqAccess<'de>, { - let field = seq.next_element()? + let field: &str = seq.next_element()? .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; $name::from_id_or_uri(field) + .map($name::into_static) .map_err(serde::de::Error::custom) } } @@ -380,37 +419,20 @@ macro_rules! define_impls { } } - /// Cheap conversion to `str` - impl AsRef for $name { - fn as_ref(&self) -> &str { - self.id() - } - } - /// `Id`s may be borrowed as `str` the same way `Box` may be /// borrowed as `T` or `String` as `str` - impl std::borrow::Borrow for $name { + impl std::borrow::Borrow for $name<'_> { fn borrow(&self) -> &str { self.id() } } /// Displaying the ID shows its URI - impl std::fmt::Display for $name { + impl std::fmt::Display for $name<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.uri()) } } - - /// IDs can also be used to convert from a `str`; this works both - /// with IDs and URIs. - impl std::str::FromStr for $name { - type Err = IdError; - - fn from_str(s: &str) -> Result { - Self::from_id_or_uri(s) - } - } )+ } } @@ -418,73 +440,112 @@ macro_rules! define_impls { // First declaring the regular IDs. Those with custom behaviour will have to be // declared manually later on. define_idtypes!( - Artist => ArtistId, - Album => AlbumId, - Track => TrackId, - Playlist => PlaylistId, - Show => ShowId, - Episode => EpisodeId + Artist => { + name: ArtistId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + Album => { + name: AlbumId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + Track => { + name: TrackId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + Playlist => { + name: PlaylistId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + Show => { + name: ShowId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + Episode => { + name: EpisodeId, + validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric()) + }, + User => { + name: UserId, + validity: |_| true + } ); -/// ID of type [`Type::User`]. Refer to the [module-level -/// docs][`crate::idtypes`] for more information. -/// -/// Note that the implementation of this specific ID differs from the rest: a -/// user's ID doesn't necessarily have to be made of alphanumeric characters. It -/// can also use underscores and other characters, but since Spotify doesn't -/// specify it explicitly, this just allows any string as an ID. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] -pub struct UserId(String); -impl Id for UserId { - #[inline] - fn id(&self) -> &str { - &self.0 +// We use `enum_dispatch` for dynamic dispatch, which is not only easier to use +// than `dyn`, but also more efficient. +/// Grouping up multiple kinds of IDs to treat them generically. This also +/// implements [`Id`], and [`From`] to instantiate it. +#[enum_dispatch(Id)] +pub enum PlayContextId<'a> { + Artist(ArtistId<'a>), + Album(AlbumId<'a>), + Playlist(PlaylistId<'a>), + Show(ShowId<'a>), +} +// These don't work with `enum_dispatch`, unfortunately. +impl<'a> PlayContextId<'a> { + pub fn as_ref(&'a self) -> Self { + match self { + PlayContextId::Artist(x) => PlayContextId::Artist(x.as_ref()), + PlayContextId::Album(x) => PlayContextId::Album(x.as_ref()), + PlayContextId::Playlist(x) => PlayContextId::Playlist(x.as_ref()), + PlayContextId::Show(x) => PlayContextId::Show(x.as_ref()), + } } - #[inline] - fn _type(&self) -> Type { - Type::User + pub fn into_static(self) -> PlayContextId<'static> { + match self { + PlayContextId::Artist(x) => PlayContextId::Artist(x.into_static()), + PlayContextId::Album(x) => PlayContextId::Album(x.into_static()), + PlayContextId::Playlist(x) => PlayContextId::Playlist(x.into_static()), + PlayContextId::Show(x) => PlayContextId::Show(x.into_static()), + } } - #[inline] - fn _type_static() -> Type - where - Self: Sized, - { - Type::User + pub fn clone_static(&'a self) -> PlayContextId<'static> { + match self { + PlayContextId::Artist(x) => PlayContextId::Artist(x.clone_static()), + PlayContextId::Album(x) => PlayContextId::Album(x.clone_static()), + PlayContextId::Playlist(x) => PlayContextId::Playlist(x.clone_static()), + PlayContextId::Show(x) => PlayContextId::Show(x.clone_static()), + } } +} - #[inline] - unsafe fn from_id_unchecked(id: &str) -> Self { - UserId(id.to_owned()) +/// Grouping up multiple kinds of IDs to treat them generically. This also +/// implements [`Id`] and [`From`] to instantiate it. +#[enum_dispatch(Id)] +pub enum PlayableId<'a> { + Track(TrackId<'a>), + Episode(EpisodeId<'a>), +} +// These don't work with `enum_dispatch`, unfortunately. +impl<'a> PlayableId<'a> { + pub fn as_ref(&'a self) -> Self { + match self { + PlayableId::Track(x) => PlayableId::Track(x.as_ref()), + PlayableId::Episode(x) => PlayableId::Episode(x.as_ref()), + } } - /// Parse Spotify id from string slice. Spotify doesn't specify what a User - /// ID might look like, so this will allow any kind of value. - fn from_id(id: &str) -> Result - where - Self: Sized, - { - // Safe, we've just checked that the Id is valid. - Ok(unsafe { Self::from_id_unchecked(id) }) + pub fn into_static(self) -> PlayableId<'static> { + match self { + PlayableId::Track(x) => PlayableId::Track(x.into_static()), + PlayableId::Episode(x) => PlayableId::Episode(x.into_static()), + } } -} - -// Finally implement some common traits for the ID types. -define_impls!(ArtistId, AlbumId, TrackId, PlaylistId, UserId, ShowId, EpisodeId); -// Grouping up the IDs into more specific traits -impl PlayContextId for ArtistId {} -impl PlayContextId for AlbumId {} -impl PlayContextId for PlaylistId {} -impl PlayContextId for ShowId {} -impl PlayableId for TrackId {} -impl PlayableId for EpisodeId {} + pub fn clone_static(&'a self) -> PlayableId<'static> { + match self { + PlayableId::Track(x) => PlayableId::Track(x.clone_static()), + PlayableId::Episode(x) => PlayableId::Episode(x.clone_static()), + } + } +} #[cfg(test)] mod test { use super::*; - use std::error::Error; + use std::{borrow::Cow, error::Error}; // Valid values: const ID: &str = "4iV5W9uYEdYUVa79Axb7Rh"; @@ -526,7 +587,7 @@ mod test { fn test_id_or_uri_and_deserialize() { fn test_any(check: F) where - F: Fn(&str) -> Result, + F: Fn(&str) -> Result, E>, E: Error, { // In this case we also check that the contents are the ID and not @@ -549,7 +610,7 @@ mod test { // Easily testing both ways to obtain an ID test_any(|s| TrackId::from_id_or_uri(s)); test_any(|s| { - let json = format!("\"{}\"", s); + let json = format!("\"{s}\""); serde_json::from_str::<'_, TrackId>(&json) }); } @@ -557,7 +618,7 @@ mod test { /// Serializing should return the Id within it, not the URI. #[test] fn test_serialize() { - let json_expected = format!("\"{}\"", ID); + let json_expected = format!("\"{ID}\""); let track = TrackId::from_uri(URI).unwrap(); let json = serde_json::to_string(&track).unwrap(); assert_eq!(json, json_expected); @@ -565,29 +626,61 @@ mod test { #[test] fn test_multiple_types() { - fn endpoint(_ids: impl IntoIterator>) {} + fn endpoint<'a>(_ids: impl IntoIterator>) {} - let tracks: [Box; 4] = [ - Box::new(TrackId::from_id(ID).unwrap()), - Box::new(TrackId::from_id(ID).unwrap()), - Box::new(EpisodeId::from_id(ID).unwrap()), - Box::new(EpisodeId::from_id(ID).unwrap()), + let tracks: Vec = vec![ + PlayableId::Track(TrackId::from_id(ID).unwrap()), + PlayableId::Track(TrackId::from_id(ID).unwrap()), + PlayableId::Episode(EpisodeId::from_id(ID).unwrap()), + PlayableId::Episode(EpisodeId::from_id(ID).unwrap()), ]; endpoint(tracks); } #[test] fn test_unknown_at_compile_time() { - fn endpoint1(input: &str, is_episode: bool) -> Box { + fn endpoint1(input: &str, is_episode: bool) -> PlayableId<'_> { if is_episode { - Box::new(EpisodeId::from_id(input).unwrap()) + PlayableId::Episode(EpisodeId::from_id(input).unwrap()) } else { - Box::new(TrackId::from_id(input).unwrap()) + PlayableId::Track(TrackId::from_id(input).unwrap()) } } - fn endpoint2(_id: &[Box]) {} + fn endpoint2(_id: &[PlayableId]) {} let id = endpoint1(ID, false); endpoint2(&[id]); } + + #[test] + fn test_constructor() { + // With `&str` + let _ = EpisodeId::from_id(ID).unwrap(); + // With `String` + let _ = EpisodeId::from_id(ID.to_string()).unwrap(); + // With borrowed `Cow` + let _ = EpisodeId::from_id(Cow::Borrowed(ID)).unwrap(); + // With owned `Cow` + let _ = EpisodeId::from_id(Cow::Owned(ID.to_string())).unwrap(); + } + + #[test] + fn test_owned() { + // We check it twice to make sure cloning statically also works. + fn check_static(_: EpisodeId<'static>) {} + + // With lifetime smaller than static because it's a locally owned + // variable. + let local_id = String::from(ID); + + // With `&str`: should be converted + let id: EpisodeId<'_> = EpisodeId::from_id(local_id.as_str()).unwrap(); + check_static(id.clone_static()); + check_static(id.into_static()); + + // With `String`: already static + let id = EpisodeId::from_id(local_id.clone()).unwrap(); + check_static(id.clone()); + check_static(id); + } } diff --git a/rspotify-model/src/lib.rs b/rspotify-model/src/lib.rs index f4012837..d0b1242d 100644 --- a/rspotify-model/src/lib.rs +++ b/rspotify-model/src/lib.rs @@ -53,10 +53,11 @@ impl PlayableItem { /// /// Note that if it's a track and if it's local, it may not have an ID, in /// which case this function will return `None`. - pub fn id(&self) -> Option<&dyn PlayableId> { + #[must_use] + pub fn id(&self) -> Option> { match self { - PlayableItem::Track(t) => t.id.as_ref().map(|t| t as &dyn PlayableId), - PlayableItem::Episode(e) => Some(&e.id), + PlayableItem::Track(t) => t.id.as_ref().map(|t| PlayableId::Track(t.as_ref())), + PlayableItem::Episode(e) => Some(PlayableId::Episode(e.id.as_ref())), } } } diff --git a/rspotify-model/src/playlist.rs b/rspotify-model/src/playlist.rs index 00e51387..1a8fa36a 100644 --- a/rspotify-model/src/playlist.rs +++ b/rspotify-model/src/playlist.rs @@ -26,7 +26,7 @@ pub struct SimplifiedPlaylist { pub collaborative: bool, pub external_urls: HashMap, pub href: String, - pub id: PlaylistId, + pub id: PlaylistId<'static>, pub images: Vec, pub name: String, pub owner: PublicUser, @@ -43,7 +43,7 @@ pub struct FullPlaylist { pub external_urls: HashMap, pub followers: Followers, pub href: String, - pub id: PlaylistId, + pub id: PlaylistId<'static>, pub images: Vec, pub name: String, pub owner: PublicUser, diff --git a/rspotify-model/src/show.rs b/rspotify-model/src/show.rs index bcd05889..f9a89ff0 100644 --- a/rspotify-model/src/show.rs +++ b/rspotify-model/src/show.rs @@ -24,7 +24,7 @@ pub struct SimplifiedShow { pub explicit: bool, pub external_urls: HashMap, pub href: String, - pub id: ShowId, + pub id: ShowId<'static>, pub images: Vec, pub is_externally_hosted: Option, pub languages: Vec, @@ -56,7 +56,7 @@ pub struct FullShow { pub episodes: Page, pub external_urls: HashMap, pub href: String, - pub id: ShowId, + pub id: ShowId<'static>, pub images: Vec, pub is_externally_hosted: Option, pub languages: Vec, @@ -75,7 +75,7 @@ pub struct SimplifiedEpisode { pub explicit: bool, pub external_urls: HashMap, pub href: String, - pub id: EpisodeId, + pub id: EpisodeId<'static>, pub images: Vec, pub is_externally_hosted: bool, pub is_playable: bool, @@ -100,7 +100,7 @@ pub struct FullEpisode { pub explicit: bool, pub external_urls: HashMap, pub href: String, - pub id: EpisodeId, + pub id: EpisodeId<'static>, pub images: Vec, pub is_externally_hosted: bool, pub is_playable: bool, diff --git a/rspotify-model/src/track.rs b/rspotify-model/src/track.rs index f684ad68..dbcd7696 100644 --- a/rspotify-model/src/track.rs +++ b/rspotify-model/src/track.rs @@ -24,7 +24,7 @@ pub struct FullTrack { pub external_urls: HashMap, pub href: Option, /// Note that a track may not have an ID/URI if it's local - pub id: Option, + pub id: Option>, pub is_local: bool, #[serde(skip_serializing_if = "Option::is_none")] pub is_playable: Option, @@ -43,7 +43,7 @@ pub struct FullTrack { pub struct TrackLink { pub external_urls: HashMap, pub href: String, - pub id: TrackId, + pub id: TrackId<'static>, } /// Intermediate full track wrapped by `Vec` @@ -67,7 +67,7 @@ pub struct SimplifiedTrack { pub external_urls: HashMap, #[serde(default)] pub href: Option, - pub id: Option, + pub id: Option>, pub is_local: bool, pub is_playable: Option, pub linked_from: Option, @@ -86,10 +86,10 @@ pub struct SavedTrack { /// Track id with specific positions track in a playlist /// -/// This is a short-lived struct for endpoint parameters, so it uses `&dyn -/// PlayableId` instead of `Box` to avoid the unnecessary +/// This is a short-lived struct for endpoint parameters, so it uses +/// `PlayableId<'a>` instead of `PlayableId<'static>` to avoid the unnecessary /// allocation. Same goes for the positions slice instead of vector. pub struct ItemPositions<'a> { - pub id: &'a dyn PlayableId, + pub id: PlayableId<'a>, pub positions: &'a [u32], } diff --git a/rspotify-model/src/user.rs b/rspotify-model/src/user.rs index 5fec1ed4..86978760 100644 --- a/rspotify-model/src/user.rs +++ b/rspotify-model/src/user.rs @@ -13,7 +13,7 @@ pub struct PublicUser { pub external_urls: HashMap, pub followers: Option, pub href: String, - pub id: UserId, + pub id: UserId<'static>, #[serde(default = "Vec::new")] pub images: Vec, } @@ -28,7 +28,7 @@ pub struct PrivateUser { pub explicit_content: Option, pub followers: Option, pub href: String, - pub id: UserId, + pub id: UserId<'static>, pub images: Option>, pub product: Option, } diff --git a/src/clients/base.rs b/src/clients/base.rs index efe12366..392b10b5 100644 --- a/src/clients/base.rs +++ b/src/clients/base.rs @@ -244,7 +244,7 @@ where /// - track_id - a spotify URI, URL or ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-track) - async fn track(&self, track_id: &TrackId) -> ClientResult { + async fn track(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("tracks/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result(&result) @@ -259,13 +259,13 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-several-tracks) async fn tracks<'a>( &self, - track_ids: impl IntoIterator + Send + 'a, + track_ids: impl IntoIterator> + Send + 'a, market: Option, ) -> ClientResult> { let ids = join_ids(track_ids); let params = build_map([("market", market.map(|x| x.into()))]); - let url = format!("tracks/?ids={}", ids); + let url = format!("tracks/?ids={ids}"); let result = self.endpoint_get(&url, ¶ms).await?; convert_result::(&result).map(|x| x.tracks) } @@ -276,7 +276,7 @@ where /// - artist_id - an artist ID, URI or URL /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artist) - async fn artist(&self, artist_id: &ArtistId) -> ClientResult { + async fn artist(&self, artist_id: ArtistId<'_>) -> ClientResult { let url = format!("artists/{}", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result(&result) @@ -290,10 +290,10 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-artists) async fn artists<'a>( &self, - artist_ids: impl IntoIterator + Send + 'a, + artist_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let ids = join_ids(artist_ids); - let url = format!("artists/?ids={}", ids); + let url = format!("artists/?ids={ids}"); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result::(&result).map(|x| x.artists) @@ -314,7 +314,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-albums) fn artist_albums<'a>( &'a self, - artist_id: &'a ArtistId, + artist_id: &'a ArtistId<'_>, album_type: Option, market: Option, ) -> Paginator<'_, ClientResult> { @@ -329,7 +329,7 @@ where /// The manually paginated version of [`Self::artist_albums`]. async fn artist_albums_manual( &self, - artist_id: &ArtistId, + artist_id: &ArtistId<'_>, album_type: Option, market: Option, limit: Option, @@ -359,7 +359,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-top-tracks) async fn artist_top_tracks( &self, - artist_id: &ArtistId, + artist_id: ArtistId<'_>, market: Market, ) -> ClientResult> { let params = build_map([("market", Some(market.into()))]); @@ -377,7 +377,10 @@ where /// - artist_id - the artist ID, URI or URL /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-related-artists) - async fn artist_related_artists(&self, artist_id: &ArtistId) -> ClientResult> { + async fn artist_related_artists( + &self, + artist_id: ArtistId<'_>, + ) -> ClientResult> { let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result::(&result).map(|x| x.artists) @@ -389,7 +392,7 @@ where /// - album_id - the album ID, URI or URL /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-album) - async fn album(&self, album_id: &AlbumId) -> ClientResult { + async fn album(&self, album_id: AlbumId<'_>) -> ClientResult { let url = format!("albums/{}", album_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; @@ -404,10 +407,10 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-albums) async fn albums<'a>( &self, - album_ids: impl IntoIterator + Send + 'a, + album_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let ids = join_ids(album_ids); - let url = format!("albums/?ids={}", ids); + let url = format!("albums/?ids={ids}"); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result::(&result).map(|x| x.albums) } @@ -464,7 +467,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-albums-tracks) fn album_track<'a>( &'a self, - album_id: &'a AlbumId, + album_id: &'a AlbumId<'_>, ) -> Paginator<'_, ClientResult> { paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), @@ -475,7 +478,7 @@ where /// The manually paginated version of [`Self::album_track`]. async fn album_track_manual( &self, - album_id: &AlbumId, + album_id: &AlbumId<'_>, limit: Option, offset: Option, ) -> ClientResult> { @@ -494,7 +497,7 @@ where /// - user - the id of the usr /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-profile) - async fn user(&self, user_id: &UserId) -> ClientResult { + async fn user(&self, user_id: UserId<'_>) -> ClientResult { let url = format!("users/{}", user_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result(&result) @@ -509,7 +512,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-playlist) async fn playlist( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -530,8 +533,8 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-list-users-playlists) async fn user_playlist( &self, - user_id: &UserId, - playlist_id: Option<&PlaylistId>, + user_id: UserId<'_>, + playlist_id: Option>, fields: Option<&str>, ) -> ClientResult { let params = build_map([("fields", fields)]); @@ -554,8 +557,8 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-if-user-follows-playlist) async fn playlist_check_follow( &self, - playlist_id: &PlaylistId, - user_ids: &[&UserId], + playlist_id: PlaylistId<'_>, + user_ids: &[UserId<'_>], ) -> ClientResult> { debug_assert!( user_ids.len() <= 5, @@ -583,7 +586,7 @@ where /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-show) - async fn get_a_show(&self, id: &ShowId, market: Option) -> ClientResult { + async fn get_a_show(&self, id: ShowId<'_>, market: Option) -> ClientResult { let params = build_map([("market", market.map(|x| x.into()))]); let url = format!("shows/{}", id.id()); @@ -601,7 +604,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-shows) async fn get_several_shows<'a>( &self, - ids: impl IntoIterator + Send + 'a, + ids: impl IntoIterator> + Send + 'a, market: Option, ) -> ClientResult> { let ids = join_ids(ids); @@ -628,7 +631,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-shows-episodes) fn get_shows_episodes<'a>( &'a self, - id: &'a ShowId, + id: &'a ShowId<'_>, market: Option, ) -> Paginator<'_, ClientResult> { paginate( @@ -642,7 +645,7 @@ where /// The manually paginated version of [`Self::get_shows_episodes`]. async fn get_shows_episodes_manual( &self, - id: &ShowId, + id: &ShowId<'_>, market: Option, limit: Option, offset: Option, @@ -671,7 +674,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-episode) async fn get_an_episode( &self, - id: &EpisodeId, + id: EpisodeId<'_>, market: Option, ) -> ClientResult { let url = format!("episodes/{}", id.id()); @@ -690,7 +693,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-episodes) async fn get_several_episodes<'a>( &self, - ids: impl IntoIterator + Send + 'a, + ids: impl IntoIterator> + Send + 'a, market: Option, ) -> ClientResult> { let ids = join_ids(ids); @@ -706,7 +709,7 @@ where /// - track - track URI, URL or ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-audio-features) - async fn track_features(&self, track_id: &TrackId) -> ClientResult { + async fn track_features(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("audio-features/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result(&result) @@ -720,7 +723,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-several-audio-features) async fn tracks_features<'a>( &self, - track_ids: impl IntoIterator + Send + 'a, + track_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult>> { let url = format!("audio-features/?ids={}", join_ids(track_ids)); @@ -739,7 +742,7 @@ where /// - track_id - a track URI, URL or ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-audio-analysis) - async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { + async fn track_analysis(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("audio-analysis/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; convert_result(&result) @@ -834,7 +837,7 @@ where ("offset", offset.as_deref()), ]); - let url = format!("browse/categories/{}/playlists", category_id); + let url = format!("browse/categories/{category_id}/playlists"); let result = self.endpoint_get(&url, ¶ms).await?; convert_result::(&result).map(|x| x.playlists) } @@ -943,9 +946,9 @@ where async fn recommendations<'a>( &self, attributes: impl IntoIterator + Send + 'a, - seed_artists: Option + Send + 'a>, + seed_artists: Option> + Send + 'a>, seed_genres: Option + Send + 'a>, - seed_tracks: Option + Send + 'a>, + seed_tracks: Option> + Send + 'a>, market: Option, limit: Option, ) -> ClientResult { @@ -993,13 +996,19 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-playlists-tracks) fn playlist_items<'a>( &'a self, - playlist_id: &'a PlaylistId, + playlist_id: &'a PlaylistId<'_>, fields: Option<&'a str>, market: Option, ) -> Paginator<'_, ClientResult> { paginate( move |limit, offset| { - self.playlist_items_manual(playlist_id, fields, market, Some(limit), Some(offset)) + self.playlist_items_manual( + playlist_id.as_ref(), + fields, + market, + Some(limit), + Some(offset), + ) }, self.get_config().pagination_chunks, ) @@ -1008,7 +1017,7 @@ where /// The manually paginated version of [`Self::playlist_items`]. async fn playlist_items_manual( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, fields: Option<&str>, market: Option, limit: Option, @@ -1041,7 +1050,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-list-users-playlists) fn user_playlists<'a>( &'a self, - user_id: &'a UserId, + user_id: &'a UserId<'_>, ) -> Paginator<'_, ClientResult> { paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), @@ -1052,7 +1061,7 @@ where /// The manually paginated version of [`Self::user_playlists`]. async fn user_playlists_manual( &self, - user_id: &UserId, + user_id: &UserId<'_>, limit: Option, offset: Option, ) -> ClientResult> { diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 5a648bbf..84464431 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -7,6 +7,8 @@ pub use oauth::OAuthClient; use crate::ClientResult; +use std::fmt::Write as _; + use serde::Deserialize; /// Converts a JSON response from Spotify into its model. @@ -17,11 +19,11 @@ pub(in crate) fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> Clien /// Append device ID to an API path. pub(in crate) fn append_device_id(path: &str, device_id: Option<&str>) -> String { let mut new_path = path.to_string(); - if let Some(_device_id) = device_id { + if let Some(device_id) = device_id { if path.contains('?') { - new_path.push_str(&format!("&device_id={}", _device_id)); + let _ = write!(new_path, "&device_id={device_id}"); } else { - new_path.push_str(&format!("?device_id={}", _device_id)); + let _ = write!(new_path, "?device_id={device_id}"); } } new_path diff --git a/src/clients/oauth.rs b/src/clients/oauth.rs index 44aa5792..44d6e4a0 100644 --- a/src/clients/oauth.rs +++ b/src/clients/oauth.rs @@ -15,7 +15,7 @@ use crate::{ use std::{collections::HashMap, time}; use maybe_async::maybe_async; -use rspotify_model::idtypes::PlayContextId; +use rspotify_model::idtypes::{PlayContextId, PlayableId}; use serde_json::{json, Map}; use url::Url; @@ -223,7 +223,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/create-playlist) async fn user_playlist_create( &self, - user_id: &UserId, + user_id: UserId<'_>, name: &str, public: Option, collaborative: Option, @@ -253,7 +253,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/change-playlist-details) async fn playlist_change_detail( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, name: Option<&str>, public: Option, description: Option<&str>, @@ -276,7 +276,7 @@ pub trait OAuthClient: BaseClient { /// - playlist_id - the id of the playlist /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-playlist) - async fn playlist_unfollow(&self, playlist_id: &PlaylistId) -> ClientResult<()> { + async fn playlist_unfollow(&self, playlist_id: PlaylistId<'_>) -> ClientResult<()> { let url = format!("playlists/{}/followers", playlist_id.id()); self.endpoint_delete(&url, &json!({})).await?; @@ -293,8 +293,8 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/add-tracks-to-playlist) async fn playlist_add_items<'a>( &self, - playlist_id: &PlaylistId, - items: impl IntoIterator + Send + 'a, + playlist_id: PlaylistId<'_>, + items: impl IntoIterator> + Send + 'a, position: Option, ) -> ClientResult { let uris = items.into_iter().map(|id| id.uri()).collect::>(); @@ -318,8 +318,8 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/reorder-or-replace-playlists-tracks) async fn playlist_replace_items<'a>( &self, - playlist_id: &PlaylistId, - items: impl IntoIterator + Send + 'a, + playlist_id: PlaylistId<'_>, + items: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let uris = items.into_iter().map(|id| id.uri()).collect::>(); let params = build_json! { @@ -346,7 +346,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/reorder-or-replace-playlists-tracks) async fn playlist_reorder_items( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, range_start: Option, insert_before: Option, range_length: Option, @@ -374,8 +374,8 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-playlist) async fn playlist_remove_all_occurrences_of_items<'a>( &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator + Send + 'a, + playlist_id: PlaylistId<'_>, + track_ids: impl IntoIterator> + Send + 'a, snapshot_id: Option<&str>, ) -> ClientResult { let tracks = track_ids @@ -428,7 +428,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-playlist) async fn playlist_remove_specific_occurrences_of_items<'a>( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, items: impl IntoIterator> + Send + 'a, snapshot_id: Option<&str>, ) -> ClientResult { @@ -460,7 +460,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-playlist) async fn playlist_follow( &self, - playlist_id: &PlaylistId, + playlist_id: PlaylistId<'_>, public: Option, ) -> ClientResult<()> { let url = format!("playlists/{}/followers", playlist_id.id()); @@ -622,7 +622,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-user) async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator + Send + 'a, + track_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -639,7 +639,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-tracks) async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator + Send + 'a, + track_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; @@ -654,7 +654,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-tracks-user) async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator + Send + 'a, + track_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -785,7 +785,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-albums-user) async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator + Send + 'a, + album_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -801,7 +801,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-albums-user) async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator + Send + 'a, + album_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -818,7 +818,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-albums) async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator + Send + 'a, + album_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; @@ -833,7 +833,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-artists-users) async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator + Send + 'a, + artist_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -849,7 +849,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-artists-users) async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator + Send + 'a, + artist_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -866,7 +866,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-current-user-follows) async fn user_artist_check_follow<'a>( &self, - artist_ids: impl IntoIterator + Send + 'a, + artist_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", @@ -884,7 +884,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-artists-users) async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator + Send + 'a, + user_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -900,7 +900,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-artists-users) async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator + Send + 'a, + user_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -1021,9 +1021,9 @@ pub trait OAuthClient: BaseClient { /// - position_ms - Indicates from what position to start playback. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/start-a-users-playback) - async fn start_context_playback( + async fn start_context_playback( &self, - context_uri: &T, + context_uri: PlayContextId<'_>, device_id: Option<&str>, offset: Option, position_ms: Option, @@ -1055,7 +1055,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/start-a-users-playback) async fn start_uris_playback<'a>( &self, - uris: impl IntoIterator + Send + 'a, + uris: impl IntoIterator> + Send + 'a, device_id: Option<&str>, offset: Option, position_ms: Option, @@ -1145,7 +1145,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/seek-to-position-in-currently-playing-track) async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { let url = append_device_id( - &format!("me/player/seek?position_ms={}", position_ms), + &format!("me/player/seek?position_ms={position_ms}"), device_id, ); self.endpoint_put(&url, &json!({})).await?; @@ -1183,7 +1183,7 @@ pub trait OAuthClient: BaseClient { "volume must be between 0 and 100, inclusive" ); let url = append_device_id( - &format!("me/player/volume?volume_percent={}", volume_percent), + &format!("me/player/volume?volume_percent={volume_percent}"), device_id, ); self.endpoint_put(&url, &json!({})).await?; @@ -1199,7 +1199,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/toggle-shuffle-for-users-playback) async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { - let url = append_device_id(&format!("me/player/shuffle?state={}", state), device_id); + let url = append_device_id(&format!("me/player/shuffle?state={state}"), device_id); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1214,9 +1214,9 @@ pub trait OAuthClient: BaseClient { /// targeted /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/add-to-queue) - async fn add_item_to_queue( + async fn add_item_to_queue( &self, - item: &T, + item: PlayableId<'_>, device_id: Option<&str>, ) -> ClientResult<()> { let url = append_device_id(&format!("me/player/queue?uri={}", item.uri()), device_id); @@ -1234,7 +1234,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-shows-user) async fn save_shows<'a>( &self, - show_ids: impl IntoIterator + Send + 'a, + show_ids: impl IntoIterator> + Send + 'a, ) -> ClientResult<()> { let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -1284,7 +1284,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-shows) async fn check_users_saved_shows<'a>( &self, - ids: impl IntoIterator + Send + 'a, + ids: impl IntoIterator> + Send + 'a, ) -> ClientResult> { let ids = join_ids(ids); let params = build_map([("ids", Some(&ids))]); @@ -1302,7 +1302,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-shows-user) async fn remove_users_saved_shows<'a>( &self, - show_ids: impl IntoIterator + Send + 'a, + show_ids: impl IntoIterator> + Send + 'a, country: Option, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); diff --git a/src/lib.rs b/src/lib.rs index 3b474393..2c1c9f0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -287,8 +287,9 @@ pub(in crate) fn generate_random_string(length: usize, alphabet: &[u8]) -> Strin } #[inline] -pub(in crate) fn join_ids<'a, T: Id + 'a + ?Sized>(ids: impl IntoIterator) -> String { - ids.into_iter().map(Id::id).collect::>().join(",") +pub(in crate) fn join_ids<'a, T: Id + 'a>(ids: impl IntoIterator) -> String { + let ids = ids.into_iter().collect::>(); + ids.iter().map(Id::id).collect::>().join(",") } #[inline] diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 6eb3e7ab..0d844999 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -1,5 +1,5 @@ use rspotify::{ - model::{AlbumId, AlbumType, ArtistId, Country, Id, Market, PlaylistId, TrackId, UserId}, + model::{AlbumId, AlbumType, ArtistId, Country, Market, PlaylistId, TrackId, UserId}, prelude::*, ClientCredsSpotify, Credentials, }; @@ -26,15 +26,15 @@ pub async fn creds_client() -> ClientCredsSpotify { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_album() { let birdy_uri = AlbumId::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); - creds_client().await.album(&birdy_uri).await.unwrap(); + creds_client().await.album(birdy_uri).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_albums() { let track_uris = [ - &AlbumId::from_uri("spotify:album:41MnTivkwTO3UUJ8DrqEJJ").unwrap(), - &AlbumId::from_uri("spotify:album:6JWc4iAiJ9FjyK0B59ABb4").unwrap(), - &AlbumId::from_uri("spotify:album:6UXCm6bOO4gFlDQZV5yL37").unwrap(), + AlbumId::from_uri("spotify:album:41MnTivkwTO3UUJ8DrqEJJ").unwrap(), + AlbumId::from_uri("spotify:album:6JWc4iAiJ9FjyK0B59ABb4").unwrap(), + AlbumId::from_uri("spotify:album:6UXCm6bOO4gFlDQZV5yL37").unwrap(), ]; creds_client().await.albums(track_uris).await.unwrap(); } @@ -54,7 +54,7 @@ async fn test_artist_related_artists() { let birdy_uri = ArtistId::from_uri("spotify:artist:43ZHCT0cAZBISjO8DG9PnE").unwrap(); creds_client() .await - .artist_related_artists(&birdy_uri) + .artist_related_artists(birdy_uri) .await .unwrap(); } @@ -62,7 +62,7 @@ async fn test_artist_related_artists() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_artist() { let birdy_uri = ArtistId::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); - creds_client().await.artist(&birdy_uri).await.unwrap(); + creds_client().await.artist(birdy_uri).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] @@ -84,8 +84,8 @@ async fn test_artists_albums() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_artists() { let artist_uris = [ - &ArtistId::from_uri("spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy").unwrap(), - &ArtistId::from_uri("spotify:artist:3dBVyJ7JuOMt4GE9607Qin").unwrap(), + ArtistId::from_uri("spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy").unwrap(), + ArtistId::from_uri("spotify:artist:3dBVyJ7JuOMt4GE9607Qin").unwrap(), ]; creds_client().await.artists(artist_uris).await.unwrap(); } @@ -95,7 +95,7 @@ async fn test_artist_top_tracks() { let birdy_uri = ArtistId::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client() .await - .artist_top_tracks(&birdy_uri, Market::Country(Country::UnitedStates)) + .artist_top_tracks(birdy_uri, Market::Country(Country::UnitedStates)) .await .unwrap(); } @@ -103,13 +103,13 @@ async fn test_artist_top_tracks() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_audio_analysis() { let track = TrackId::from_id("06AKEBrKUckW0KREUWRnvT").unwrap(); - creds_client().await.track_analysis(&track).await.unwrap(); + creds_client().await.track_analysis(track).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_audio_features() { let track = TrackId::from_uri("spotify:track:06AKEBrKUckW0KREUWRnvT").unwrap(); - creds_client().await.track_features(&track).await.unwrap(); + creds_client().await.track_features(track).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] @@ -121,7 +121,7 @@ async fn test_audios_features() { tracks_ids.push(track_id2); creds_client() .await - .tracks_features(&tracks_ids) + .tracks_features(tracks_ids) .await .unwrap(); } @@ -129,20 +129,20 @@ async fn test_audios_features() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_user() { let birdy_uri = UserId::from_id("tuggareutangranser").unwrap(); - creds_client().await.user(&birdy_uri).await.unwrap(); + creds_client().await.user(birdy_uri).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_track() { let birdy_uri = TrackId::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); - creds_client().await.track(&birdy_uri).await.unwrap(); + creds_client().await.track(birdy_uri).await.unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_tracks() { let track_uris = [ - &TrackId::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(), - &TrackId::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(), + TrackId::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(), + TrackId::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(), ]; creds_client().await.tracks(track_uris, None).await.unwrap(); } @@ -152,7 +152,7 @@ async fn test_existing_playlist() { let playlist_id = PlaylistId::from_id("37i9dQZF1DZ06evO45P0Eo").unwrap(); creds_client() .await - .playlist(&playlist_id, None, None) + .playlist(playlist_id, None, None) .await .unwrap(); } @@ -160,10 +160,7 @@ async fn test_existing_playlist() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] async fn test_fake_playlist() { let playlist_id = PlaylistId::from_id("fakeid").unwrap(); - let playlist = creds_client() - .await - .playlist(&playlist_id, None, None) - .await; + let playlist = creds_client().await.playlist(playlist_id, None, None).await; assert!(playlist.is_err()); } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index c03f25b0..f42469b8 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -195,15 +195,15 @@ async fn test_current_user_recently_played() { #[ignore] async fn test_current_user_saved_albums() { let album_ids = [ - &AlbumId::from_id("6akEvsycLGftJxYudPjmqK").unwrap(), - &AlbumId::from_id("628oezqK2qfmCjC6eXNors").unwrap(), + AlbumId::from_id("6akEvsycLGftJxYudPjmqK").unwrap(), + AlbumId::from_id("628oezqK2qfmCjC6eXNors").unwrap(), ]; let client = oauth_client().await; // First adding the albums client - .current_user_saved_albums_add(album_ids) + .current_user_saved_albums_add(album_ids.iter().map(AlbumId::as_ref)) .await .unwrap(); @@ -233,16 +233,16 @@ async fn test_current_user_saved_albums() { async fn test_current_user_saved_tracks_add() { let client = oauth_client().await; let tracks_ids = [ - &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), - &TrackId::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(), + TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), + TrackId::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(), ]; client - .current_user_saved_tracks_add(tracks_ids) + .current_user_saved_tracks_add(tracks_ids.iter().map(TrackId::as_ref)) .await .unwrap(); let contains = client - .current_user_saved_tracks_contains(tracks_ids) + .current_user_saved_tracks_contains(tracks_ids.iter().map(TrackId::as_ref)) .await .unwrap(); // Every track should be saved @@ -323,10 +323,10 @@ async fn test_new_releases_with_from_token() { #[ignore] async fn test_playback() { let client = oauth_client().await; - let uris: [&dyn PlayableId; 3] = [ - &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), - &TrackId::from_uri("spotify:track:2DzSjFQKetFhkFCuDWhioi").unwrap(), - &EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), + let uris = [ + PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:2DzSjFQKetFhkFCuDWhioi").unwrap()), + PlayableId::Episode(EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap()), ]; let devices = client.device().await.unwrap(); @@ -346,7 +346,12 @@ async fn test_playback() { // Starting playback of some songs client - .start_uris_playback(uris, Some(device_id), Some(Offset::for_position(0)), None) + .start_uris_playback( + uris.iter().map(PlayableId::as_ref), + Some(device_id), + Some(Offset::for_position(0)), + None, + ) .await .unwrap(); @@ -400,8 +405,8 @@ async fn test_playback() { #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] #[ignore] async fn test_recommendations() { - let seed_artists = [&ArtistId::from_id("4NHQUGzhtTLFvgF5SZesLK").unwrap()]; - let seed_tracks = [&TrackId::from_id("0c6xIDDpzE81m2q797ordA").unwrap()]; + let seed_artists = [ArtistId::from_id("4NHQUGzhtTLFvgF5SZesLK").unwrap()]; + let seed_tracks = [TrackId::from_id("0c6xIDDpzE81m2q797ordA").unwrap()]; let attributes = [ RecommendationsAttribute::MinEnergy(0.4), RecommendationsAttribute::MinPopularity(50), @@ -556,11 +561,14 @@ async fn test_shuffle() { async fn test_user_follow_artist() { let client = oauth_client().await; let artists = [ - &ArtistId::from_id("74ASZWbe4lXaubB36ztrGX").unwrap(), - &ArtistId::from_id("08td7MxkoHQkXnWAYD8d6Q").unwrap(), + ArtistId::from_id("74ASZWbe4lXaubB36ztrGX").unwrap(), + ArtistId::from_id("08td7MxkoHQkXnWAYD8d6Q").unwrap(), ]; - client.user_follow_artists(artists).await.unwrap(); + client + .user_follow_artists(artists.iter().map(ArtistId::as_ref)) + .await + .unwrap(); client.user_unfollow_artists(artists).await.unwrap(); } @@ -569,11 +577,14 @@ async fn test_user_follow_artist() { async fn test_user_follow_users() { let client = oauth_client().await; let users = [ - &UserId::from_id("exampleuser01").unwrap(), - &UserId::from_id("john").unwrap(), + UserId::from_id("exampleuser01").unwrap(), + UserId::from_id("john").unwrap(), ]; - client.user_follow_users(users).await.unwrap(); + client + .user_follow_users(users.iter().map(UserId::as_ref)) + .await + .unwrap(); client.user_unfollow_users(users).await.unwrap(); } @@ -584,11 +595,11 @@ async fn test_user_follow_playlist() { let playlist_id = PlaylistId::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); client - .playlist_follow(&playlist_id, Some(true)) + .playlist_follow(playlist_id.as_ref(), Some(true)) .await .unwrap(); - client.playlist_unfollow(&playlist_id).await.unwrap(); + client.playlist_unfollow(playlist_id).await.unwrap(); } #[maybe_async] @@ -598,13 +609,13 @@ async fn check_playlist_create(client: &AuthCodeSpotify) -> FullPlaylist { // First creating the base playlist over which the tests will be ran let playlist = client - .user_playlist_create(&user.id, name, Some(false), None, None) + .user_playlist_create(user.id.as_ref(), name, Some(false), None, None) .await .unwrap(); // Making sure that the playlist has been added to the user's profile let fetched_playlist = client - .user_playlist(&user.id, Some(&playlist.id), None) + .user_playlist(user.id.as_ref(), Some(playlist.id.as_ref()), None) .await .unwrap(); assert_eq!(playlist.id, fetched_playlist.id); @@ -617,7 +628,7 @@ async fn check_playlist_create(client: &AuthCodeSpotify) -> FullPlaylist { let description = "A random description"; client .playlist_change_detail( - &playlist.id, + playlist.id.as_ref(), Some(name), Some(true), Some(description), @@ -630,100 +641,124 @@ async fn check_playlist_create(client: &AuthCodeSpotify) -> FullPlaylist { } #[maybe_async] -async fn check_num_tracks(client: &AuthCodeSpotify, playlist_id: &PlaylistId, num: i32) { - let fetched_tracks = fetch_all(client.playlist_items(playlist_id, None, None)).await; +async fn check_num_tracks(client: &AuthCodeSpotify, playlist_id: PlaylistId<'_>, num: i32) { + let fetched_tracks = fetch_all(client.playlist_items(&playlist_id.as_ref(), None, None)).await; assert_eq!(fetched_tracks.len() as i32, num); } #[maybe_async] async fn check_playlist_tracks(client: &AuthCodeSpotify, playlist: &FullPlaylist) { // The tracks in the playlist, some of them repeated - let tracks: [&dyn PlayableId; 4] = [ - &TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap(), - &TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap(), - &EpisodeId::from_uri("spotify/episode/381XrGKkcdNkLwfsQ4Mh5y").unwrap(), - &EpisodeId::from_uri("spotify/episode/6O63eWrfWPvN41CsSyDXve").unwrap(), + let tracks = [ + PlayableId::Track(TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap()), + PlayableId::Episode(EpisodeId::from_uri("spotify/episode/381XrGKkcdNkLwfsQ4Mh5y").unwrap()), + PlayableId::Episode(EpisodeId::from_uri("spotify/episode/6O63eWrfWPvN41CsSyDXve").unwrap()), ]; // Firstly adding some tracks client - .playlist_add_items(&playlist.id, tracks, None) + .playlist_add_items( + playlist.id.as_ref(), + tracks.iter().map(PlayableId::as_ref), + None, + ) .await .unwrap(); - check_num_tracks(client, &playlist.id, tracks.len() as i32).await; + check_num_tracks(client, playlist.id.as_ref(), tracks.len() as i32).await; // Reordering some tracks client - .playlist_reorder_items(&playlist.id, Some(0), Some(3), Some(2), None) + .playlist_reorder_items(playlist.id.as_ref(), Some(0), Some(3), Some(2), None) .await .unwrap(); // Making sure the number of tracks is the same - check_num_tracks(client, &playlist.id, tracks.len() as i32).await; + check_num_tracks(client, playlist.id.as_ref(), tracks.len() as i32).await; // Replacing the tracks - let replaced_tracks: [&dyn PlayableId; 7] = [ - &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), - &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), - &TrackId::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(), - &EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), - &TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap(), - &EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap(), - &TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap(), + let replaced_tracks = [ + PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap()), + PlayableId::Episode(EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap()), + PlayableId::Episode(EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap()), + PlayableId::Track(TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap()), ]; client - .playlist_replace_items(&playlist.id, replaced_tracks) + .playlist_replace_items( + playlist.id.as_ref(), + replaced_tracks.iter().map(|t| t.as_ref()), + ) .await .unwrap(); // Making sure the number of tracks is updated - check_num_tracks(client, &playlist.id, replaced_tracks.len() as i32).await; + check_num_tracks(client, playlist.id.as_ref(), replaced_tracks.len() as i32).await; // Removes a few specific tracks let tracks = [ ItemPositions { - id: &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), + id: PlayableId::Track( + TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), + ), positions: &[0], }, ItemPositions { - id: &TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap(), + id: PlayableId::Track( + TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap(), + ), positions: &[4, 6], }, ]; client - .playlist_remove_specific_occurrences_of_items(&playlist.id, tracks, None) + .playlist_remove_specific_occurrences_of_items(playlist.id.as_ref(), tracks, None) .await .unwrap(); // Making sure three tracks were removed - check_num_tracks(client, &playlist.id, replaced_tracks.len() as i32 - 3).await; + check_num_tracks( + client, + playlist.id.as_ref(), + replaced_tracks.len() as i32 - 3, + ) + .await; // Removes all occurrences of two tracks - let to_remove: [&dyn PlayableId; 2] = [ - &TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), - &EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), + let to_remove = vec![ + PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()), + PlayableId::Episode(EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap()), ]; client - .playlist_remove_all_occurrences_of_items(&playlist.id, to_remove, None) + .playlist_remove_all_occurrences_of_items(playlist.id.as_ref(), to_remove, None) .await .unwrap(); // Making sure two more tracks were removed - check_num_tracks(client, &playlist.id, replaced_tracks.len() as i32 - 5).await; + check_num_tracks( + client, + playlist.id.as_ref(), + replaced_tracks.len() as i32 - 5, + ) + .await; } #[maybe_async] async fn check_playlist_follow(client: &AuthCodeSpotify, playlist: &FullPlaylist) { let user_ids = [ - &UserId::from_id("possan").unwrap(), - &UserId::from_id("elogain").unwrap(), + UserId::from_id("possan").unwrap(), + UserId::from_id("elogain").unwrap(), ]; // It's a new playlist, so it shouldn't have any followers let following = client - .playlist_check_follow(&playlist.id, &user_ids) + .playlist_check_follow(playlist.id.as_ref(), &user_ids) .await .unwrap(); assert_eq!(following, vec![false, false]); // Finally unfollowing the playlist in order to clean it up - client.playlist_unfollow(&playlist.id).await.unwrap(); + client + .playlist_unfollow(playlist.id.as_ref()) + .await + .unwrap(); } #[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))] @@ -763,10 +798,11 @@ async fn test_volume() { async fn test_add_queue() { // NOTE: unfortunately it's impossible to revert this test - let birdy_uri = TrackId::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); + let birdy_uri = + PlayableId::Track(TrackId::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap()); oauth_client() .await - .add_item_to_queue(&birdy_uri, None) + .add_item_to_queue(birdy_uri, None) .await .unwrap(); } @@ -775,8 +811,8 @@ async fn test_add_queue() { #[ignore] async fn test_get_several_shows() { let shows = [ - &ShowId::from_id("5CfCWKI5pZ28U0uOzXkDHe").unwrap(), - &ShowId::from_id("5as3aKmN2k11yfDDDSrvaZ").unwrap(), + ShowId::from_id("5CfCWKI5pZ28U0uOzXkDHe").unwrap(), + ShowId::from_id("5as3aKmN2k11yfDDDSrvaZ").unwrap(), ]; oauth_client() @@ -790,8 +826,8 @@ async fn test_get_several_shows() { #[ignore] async fn test_get_several_episodes() { let episodes = [ - &EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), - &EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap(), + EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(), + EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap(), ]; oauth_client() .await