From fcd9ace324c373513ba9ff157081608ca644e3a1 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 29 Nov 2020 10:01:12 +0300 Subject: [PATCH 01/59] initial id type proposal --- Cargo.toml | 1 + src/client.rs | 424 ++++++++++++++++++++------------------- src/model/enums/types.rs | 4 +- src/model/mod.rs | 133 ++++++++++++ 4 files changed, 350 insertions(+), 212 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27594a20..d92baee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ ureq = { version = "1.4.1", default-features = false, features = ["json", "cooki url = "2.1.1" webbrowser = { version = "0.5.5", optional = true } strum = { version = "0.20", features = ["derive"] } +itertools = "0.9.0" [dev-dependencies] env_logger = "0.8.1" diff --git a/src/client.rs b/src/client.rs index cf188618..caefe962 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,6 +15,7 @@ use super::http::{BaseClient, Query}; use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; +use itertools::Itertools; /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] @@ -53,6 +54,15 @@ pub enum ClientError { #[error("cache file error: {0}")] CacheFile(String), + + #[error("id parse error: {0:?}")] + InvalidId(IdError), +} + +impl From for ClientError { + fn from(error: IdError) -> Self { + ClientError::InvalidId(error) + } } pub type ClientResult = Result; @@ -136,51 +146,11 @@ impl Spotify { .ok_or_else(|| ClientError::InvalidAuth("no oauth configured".to_string())) } - /// TODO: should be moved into a custom type - fn get_uri(&self, _type: Type, _id: &str) -> String { - format!("spotify:{}:{}", _type.to_string(), self.get_id(_type, _id)) - } - /// Converts a JSON response from Spotify into its model. fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { serde_json::from_str::(input).map_err(Into::into) } - /// Get spotify id by type and id - /// TODO: should be rewritten and moved into a separate type for IDs - fn get_id(&self, _type: Type, id: &str) -> String { - let mut _id = id.to_owned(); - let fields: Vec<&str> = _id.split(':').collect(); - let len = fields.len(); - if len >= 3 { - if _type.to_string() != fields[len - 2] { - error!( - "expected id of type {:?} but found type {:?} {:?}", - _type, - fields[len - 2], - _id - ); - } else { - return fields[len - 1].to_owned(); - } - } - let sfields: Vec<&str> = _id.split('/').collect(); - let len: usize = sfields.len(); - if len >= 3 { - if _type.to_string() != sfields[len - 2] { - error!( - "expected id of type {:?} but found type {:?} {:?}", - _type, - sfields[len - 2], - _id - ); - } else { - return sfields[len - 1].to_owned(); - } - } - _id.to_owned() - } - /// Append device ID to an API path. fn append_device_id(&self, path: &str, device_id: Option) -> String { let mut new_path = path.to_string(); @@ -202,8 +172,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-track/) #[maybe_async] pub async fn track(&self, track_id: &str) -> ClientResult { - let trid = self.get_id(Type::Track, track_id); - let url = format!("tracks/{}", trid); + let trid = Id::from_id_or_uri(Type::Track, track_id)?; + let url = format!("tracks/{}", trid.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -222,17 +192,21 @@ impl Spotify { market: Option, ) -> ClientResult { // TODO: this can be improved - let mut ids: Vec = vec![]; - for track_id in track_ids { - ids.push(self.get_id(Type::Track, track_id)); - } + let ids = track_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; let mut params = Query::new(); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } - let url = format!("tracks/?ids={}", ids.join(",")); + let url = format!("tracks/?ids={}", ids); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -245,8 +219,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-artist/) #[maybe_async] pub async fn artist(&self, artist_id: &str) -> ClientResult { - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}", trid); + let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; + let url = format!("artists/{}", trid.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -262,11 +236,15 @@ impl Spotify { &self, artist_ids: impl IntoIterator, ) -> ClientResult { - let mut ids: Vec = vec![]; - for artist_id in artist_ids { - ids.push(self.get_id(Type::Artist, artist_id)); - } - let url = format!("artists/?ids={}", ids.join(",")); + let ids = artist_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -303,8 +281,8 @@ impl Spotify { if let Some(country) = country { params.insert("country".to_owned(), country.to_string()); } - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/albums", trid); + let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; + let url = format!("artists/{}/albums", trid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -329,8 +307,8 @@ impl Spotify { country.into().unwrap_or(Country::UnitedStates).to_string(), ); - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/top-tracks", trid); + let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; + let url = format!("artists/{}/top-tracks", trid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -345,8 +323,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-related-artists/) #[maybe_async] pub async fn artist_related_artists(&self, artist_id: &str) -> ClientResult { - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/related-artists", trid); + let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; + let url = format!("artists/{}/related-artists", trid.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -359,8 +337,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-album/) #[maybe_async] pub async fn album(&self, album_id: &str) -> ClientResult { - let trid = self.get_id(Type::Album, album_id); - let url = format!("albums/{}", trid); + let trid = Id::from_id_or_uri(Type::Album, album_id)?; + let url = format!("albums/{}", trid.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -377,11 +355,15 @@ impl Spotify { &self, album_ids: impl IntoIterator, ) -> ClientResult { - let mut ids: Vec = vec![]; - for album_id in album_ids { - ids.push(self.get_id(Type::Album, album_id)); - } - let url = format!("albums/?ids={}", ids.join(",")); + let ids = album_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -445,8 +427,8 @@ impl Spotify { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let trid = self.get_id(Type::Album, album_id); - let url = format!("albums/{}/tracks", trid); + let trid = Id::from_id_or_uri(Type::Album, album_id)?; + let url = format!("albums/{}/tracks", trid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -486,8 +468,8 @@ impl Spotify { params.insert("market".to_owned(), market.to_string()); } - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("playlists/{}", plid); + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + let url = format!("playlists/{}", plid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -548,7 +530,7 @@ impl Spotify { pub async fn user_playlist( &self, user_id: &str, - playlist_id: Option<&mut str>, + playlist_id: Option<&str>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -561,8 +543,8 @@ impl Spotify { } match playlist_id { Some(playlist_id) => { - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("users/{}/playlists/{}", user_id, plid); + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + let url = format!("users/{}/playlists/{}", user_id, plid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -602,8 +584,8 @@ impl Spotify { if let Some(fields) = fields { params.insert("fields".to_owned(), fields.to_owned()); } - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("playlists/{}/tracks", plid); + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + let url = format!("playlists/{}/tracks", plid.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -700,16 +682,18 @@ impl Spotify { track_ids: impl IntoIterator, position: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + + let uris = track_ids .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); + .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .collect::, _>>()?; + let mut params = json!({ "uris": uris }); if let Some(position) = position { json_insert!(params, "position", position); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", plid.id()); let result = self.post(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -728,15 +712,14 @@ impl Spotify { playlist_id: &str, track_ids: impl IntoIterator, ) -> ClientResult<()> { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + let uris = track_ids .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); - // let mut params = Map::new(); - // params.insert("uris".to_owned(), uris.into()); + .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .collect::, _>>()?; + let params = json!({ "uris": uris }); - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", plid.id()); self.put(&url, None, ¶ms).await?; Ok(()) @@ -761,7 +744,7 @@ impl Spotify { insert_before: i32, snapshot_id: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; let mut params = json! ({ "range_start": range_start, "range_length": range_length.into().unwrap_or(1), @@ -771,7 +754,7 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", plid.id()); let result = self.put(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -791,24 +774,25 @@ impl Spotify { track_ids: impl IntoIterator, snapshot_id: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + + let tracks = track_ids .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); + .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .collect::, _>>()?; - // TODO: this can be improved - let mut tracks: Vec> = vec![]; - for uri in uris { + let mut params = json!({ "tracks": tracks.into_iter().map(|uri| { let mut map = Map::new(); map.insert("uri".to_owned(), uri.into()); - tracks.push(map); - } - let mut params = json!({ "tracks": tracks }); + map + }).collect::>() + }); + if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + + let url = format!("playlists/{}/tracks", plid.id()); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -850,12 +834,14 @@ impl Spotify { snapshot_id: Option, ) -> ClientResult { // TODO: this can be improved - let plid = self.get_id(Type::Playlist, playlist_id); + let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; + let mut ftracks: Vec> = vec![]; for track in tracks { let mut map = Map::new(); if let Some(_uri) = track.get("uri") { - let uri = self.get_uri(Type::Track, &_uri.as_str().unwrap().to_owned()); + let uri = + Id::from_id_or_uri(Type::Track, &_uri.as_str().unwrap().to_owned())?.uri(); map.insert("uri".to_owned(), uri.into()); } if let Some(_position) = track.get("position") { @@ -868,7 +854,7 @@ impl Spotify { if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", plid.id()); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -1041,11 +1027,15 @@ impl Spotify { &self, track_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = track_ids + let ids = track_ids .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/tracks/?ids={}", ids); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1063,11 +1053,15 @@ impl Spotify { &self, track_ids: impl IntoIterator, ) -> ClientResult> { - let uris: Vec = track_ids + let ids = track_ids .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/contains/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/tracks/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1083,11 +1077,15 @@ impl Spotify { &self, track_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = track_ids + let ids = track_ids .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/tracks/?ids={}", ids); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1187,11 +1185,15 @@ impl Spotify { &self, album_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = album_ids + let ids = album_ids .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/albums/?ids={}", ids); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1208,11 +1210,15 @@ impl Spotify { &self, album_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = album_ids + let ids = album_ids .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/albums/?ids={}", ids); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1230,11 +1236,15 @@ impl Spotify { &self, album_ids: impl IntoIterator, ) -> ClientResult> { - let uris: Vec = album_ids + let ids = album_ids .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/contains/?ids={}", uris.join(",")); + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/albums/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1250,10 +1260,15 @@ impl Spotify { &self, artist_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=artist&ids={}", - artist_ids.into_iter().collect::>().join(",") - ); + let ids = artist_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/following?type=artist&ids={}", ids); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1270,10 +1285,15 @@ impl Spotify { &self, artist_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=artist&ids={}", - artist_ids.into_iter().collect::>().join(",") - ); + let ids = artist_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/following?type=artist&ids={}", ids); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1289,12 +1309,17 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artsit_ids: impl IntoIterator, + artist_ids: impl IntoIterator, ) -> ClientResult> { - let url = format!( - "me/following/contains?type=artist&ids={}", - artsit_ids.into_iter().collect::>().join(",") - ); + let ids = artist_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/following/contains?type=artist&ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1310,10 +1335,15 @@ impl Spotify { &self, user_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=user&ids={}", - user_ids.into_iter().collect::>().join(",") - ); + let ids = user_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::User, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/following?type=user&ids={}", ids); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1330,10 +1360,15 @@ impl Spotify { &self, user_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=user&ids={}", - user_ids.into_iter().collect::>().join(",") - ); + let ids = user_ids + .into_iter() + .map(|id| Id::from_id_or_uri(Type::User, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("me/following?type=user&ids={}", ids); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1533,23 +1568,35 @@ impl Spotify { if let Some(seed_artists) = seed_artists { let seed_artists_ids = seed_artists .iter() - .map(|id| self.get_id(Type::Artist, id)) - .collect::>(); - params.insert("seed_artists".to_owned(), seed_artists_ids.join(",")); + .map(|id| Id::from_id_or_uri(Type::Artist, &id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + params.insert("seed_artists".to_owned(), seed_artists_ids); } + if let Some(seed_genres) = seed_genres { params.insert("seed_genres".to_owned(), seed_genres.join(",")); } + if let Some(seed_tracks) = seed_tracks { let seed_tracks_ids = seed_tracks .iter() - .map(|id| self.get_id(Type::Track, id)) - .collect::>(); - params.insert("seed_tracks".to_owned(), seed_tracks_ids.join(",")); + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + params.insert("seed_tracks".to_owned(), seed_tracks_ids); } + if let Some(country) = country { params.insert("market".to_owned(), country.to_string()); } + let result = self.get("recommendations", None, ¶ms).await?; self.convert_result(&result) } @@ -1562,8 +1609,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-audio-features/) #[maybe_async] pub async fn track_features(&self, track: &str) -> ClientResult { - let track_id = self.get_id(Type::Track, track); - let url = format!("audio-features/{}", track_id); + let track_id = Id::from_id_or_uri(Type::Track, track)?; + let url = format!("audio-features/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1579,11 +1626,15 @@ impl Spotify { &self, tracks: impl IntoIterator, ) -> ClientResult> { - let ids: Vec = tracks + let ids = tracks .into_iter() - .map(|track| self.get_id(Type::Track, track)) - .collect(); - let url = format!("audio-features/?ids={}", ids.join(",")); + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .fold_results(String::new(), |ids, id| ids + id.id() + ",") + .map(|mut ids| { + ids.pop(); + ids + })?; + let url = format!("audio-features/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; if result.is_empty() { @@ -1601,8 +1652,8 @@ impl Spotify { /// [Reference](https://developer.spotify.com/web-api/get-audio-analysis/) #[maybe_async] pub async fn track_analysis(&self, track: &str) -> ClientResult { - let trid = self.get_id(Type::Track, track); - let url = format!("audio-analysis/{}", trid); + let trid = Id::from_id_or_uri(Type::Track, track)?; + let url = format!("audio-analysis/{}", trid.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -2133,51 +2184,4 @@ mod tests { let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } - - #[test] - fn test_get_id() { - // Assert artist - let spotify = SpotifyBuilder::default().build().unwrap(); - let artist_id = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; - let id = spotify.get_id(Type::Artist, artist_id); - assert_eq!("2WX2uTcsvV5OnS0inACecP", &id); - - // Assert album - let artist_id_a = "spotify/album/2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Album, artist_id_a) - ); - - // Mismatch type - let artist_id_b = "spotify:album:2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "spotify:album:2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Artist, artist_id_b) - ); - - // Could not split - let artist_id_c = "spotify-album-2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "spotify-album-2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Artist, artist_id_c) - ); - - let playlist_id = "spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"; - assert_eq!( - "59ZbFPES4DQwEjBpWHzrtC", - &spotify.get_id(Type::Playlist, playlist_id) - ); - } - - #[test] - fn test_get_uri() { - let spotify = SpotifyBuilder::default().build().unwrap(); - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "1301WleyT98MSxVHPZCA6M"; - let uri1 = spotify.get_uri(Type::Track, track_id1); - let uri2 = spotify.get_uri(Type::Track, track_id2); - assert_eq!(track_id1, uri1); - assert_eq!("spotify:track:1301WleyT98MSxVHPZCA6M", &uri2); - } } diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index 4ed1711c..0e7e9a76 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum::ToString; +use strum::{AsRefStr, ToString}; /// Copyright type: `C` = the copyright, `P` = the sound recording (performance) copyright. /// @@ -28,7 +28,7 @@ pub enum AlbumType { } /// Type: `artist`, `album`, `track`, `playlist`, `show` or `episode` -#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, ToString)] +#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, ToString, AsRefStr)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum Type { diff --git a/src/model/mod.rs b/src/model/mod.rs index d61b7b9f..cf4b1249 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -18,6 +18,8 @@ pub mod track; pub mod user; use serde::{Deserialize, Serialize}; +use strum::Display; +use thiserror::Error; /// Restriction object /// @@ -48,8 +50,139 @@ pub enum PlayingItem { Episode(show::FullEpisode), } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Id<'id> { + _type: Type, + id: &'id str, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] +pub enum IdError { + InvalidPrefix, + InvalidFormat, + InvalidType, + InvalidId, +} + +impl std::fmt::Display for Id<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("spotify:")?; + f.write_str(self._type.as_ref())?; + f.write_str(":")?; + f.write_str(self.id)?; + Ok(()) + } +} + +impl Id<'_> { + pub fn _type(&self) -> Type { + self._type + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn uri(&self) -> String { + format!("spotify:{}:{}", self._type.as_ref(), self.id) + } + + pub fn from_id_or_uri<'a, 'b: 'a>(_type: Type, id_or_uri: &'b str) -> Result, IdError> { + match Self::from_uri(id_or_uri) { + Ok(id) if id._type == _type => Ok(id), + Ok(_) => Err(IdError::InvalidType), + Err(IdError::InvalidPrefix) => Self::from_id(_type, id_or_uri), + Err(error) => Err(error), + } + } + + pub fn from_id<'a, 'b: 'a>(_type: Type, id: &'b str) -> Result, IdError> { + if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { + Ok(Id { _type, id }) + } else { + Err(IdError::InvalidId) + } + } + + pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { + if let Some((tpe, id)) = if uri.starts_with("spotify:") { + uri[8..].rfind(':').map(|mid| uri[8..].split_at(mid)) + } else if uri.starts_with("spotify/") { + uri[8..].rfind('/').map(|mid| uri[8..].split_at(mid)) + } else { + return Err(IdError::InvalidPrefix); + } { + let _type = match tpe { + "artist" => Type::Artist, + "album" => Type::Album, + "track" => Type::Track, + "user" => Type::User, + "playlist" => Type::Playlist, + "show" => Type::Show, + "episode" => Type::Episode, + _ => return Err(IdError::InvalidType), + }; + + Self::from_id(_type, &id[1..]) + } else { + Err(IdError::InvalidFormat) + } + } +} + pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, user::*, }; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_id() { + // Assert artist + let artist_id = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let id = Id::from_id_or_uri(Type::Artist, artist_id).unwrap(); + assert_eq!("2WX2uTcsvV5OnS0inACecP", id.id()); + + // Assert album + let album_id_a = "spotify/album/2WX2uTcsvV5OnS0inACecP"; + assert_eq!( + "2WX2uTcsvV5OnS0inACecP", + Id::from_id_or_uri(Type::Album, album_id_a).unwrap().id() + ); + + // Mismatch type + assert_eq!( + Err(IdError::InvalidType), + Id::from_id_or_uri(Type::Artist, album_id_a) + ); + + // Could not split + let artist_id_c = "spotify-album-2WX2uTcsvV5OnS0inACecP"; + assert_eq!( + Err(IdError::InvalidId), + Id::from_id_or_uri(Type::Artist, artist_id_c) + ); + + let playlist_id = "spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"; + assert_eq!( + "59ZbFPES4DQwEjBpWHzrtC", + Id::from_id_or_uri(Type::Playlist, playlist_id) + .unwrap() + .id() + ); + } + + #[test] + fn test_get_uri() { + let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; + let track_id2 = "1301WleyT98MSxVHPZCA6M"; + let id1 = Id::from_id_or_uri(Type::Track, track_id1).unwrap(); + let id2 = Id::from_id_or_uri(Type::Track, track_id2).unwrap(); + assert_eq!(track_id1, &id1.uri()); + assert_eq!("spotify:track:1301WleyT98MSxVHPZCA6M", &id2.uri()); + } +} From 58e4c19c79d14140639263636fc0e45aa91b7c7a Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 29 Nov 2020 10:27:44 +0300 Subject: [PATCH 02/59] use strip_prefix() (clippy advice) --- src/model/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/model/mod.rs b/src/model/mod.rs index cf4b1249..c50de158 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -105,10 +105,10 @@ impl Id<'_> { } pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { - if let Some((tpe, id)) = if uri.starts_with("spotify:") { - uri[8..].rfind(':').map(|mid| uri[8..].split_at(mid)) - } else if uri.starts_with("spotify/") { - uri[8..].rfind('/').map(|mid| uri[8..].split_at(mid)) + if let Some((tpe, id)) = if let Some(uri) = uri.strip_prefix("spotify:") { + uri.rfind(':').map(|mid| uri.split_at(mid)) + } else if let Some(uri) = uri.strip_prefix("spotify/") { + uri.rfind('/').map(|mid| uri.split_at(mid)) } else { return Err(IdError::InvalidPrefix); } { From c3e722e9e349c9d7b3fbe108f5c2b17d97ad8ae6 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 29 Nov 2020 11:17:29 +0300 Subject: [PATCH 03/59] add try_join() and map_try_join() --- src/client.rs | 171 +++++++++++++++++++---------------------------- src/model/mod.rs | 6 ++ 2 files changed, 75 insertions(+), 102 deletions(-) diff --git a/src/client.rs b/src/client.rs index caefe962..88cf6cb7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,6 +17,36 @@ use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; use itertools::Itertools; +pub trait TryJoin: Itertools { + fn try_join(&mut self, sep: &str) -> Result + where + Self: Iterator>, + T: AsRef, + { + let (size, _) = self.size_hint(); + let cap = size * sep.len(); + + self.fold_results(String::with_capacity(cap), |ids, id| { + ids + id.as_ref() + sep + }) + .map(|mut ids| { + ids.pop(); + ids + }) + } + + fn map_try_join(&mut self, sep: &str, func: F) -> Result + where + Self: Iterator, + F: Fn(T) -> Result, + R: AsRef, + { + self.map(func).try_join(sep) + } +} + +impl TryJoin for T where T: Itertools {} + /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] pub enum ClientError { @@ -194,12 +224,7 @@ impl Spotify { // TODO: this can be improved let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; let mut params = Query::new(); if let Some(market) = market { @@ -238,12 +263,7 @@ impl Spotify { ) -> ClientResult { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -357,12 +377,7 @@ impl Spotify { ) -> ClientResult { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1029,12 +1044,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; let url = format!("me/tracks/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1055,12 +1065,7 @@ impl Spotify { ) -> ClientResult> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; let url = format!("me/tracks/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1079,12 +1084,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; let url = format!("me/tracks/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1187,12 +1187,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; let url = format!("me/albums/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1212,12 +1207,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; let url = format!("me/albums/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1238,12 +1228,7 @@ impl Spotify { ) -> ClientResult> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; let url = format!("me/albums/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1262,12 +1247,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; let url = format!("me/following?type=artist&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1287,12 +1267,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; let url = format!("me/following?type=artist&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1313,12 +1288,7 @@ impl Spotify { ) -> ClientResult> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; let url = format!("me/following/contains?type=artist&ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1337,12 +1307,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::User, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::User, id))?; let url = format!("me/following?type=user&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1362,12 +1327,7 @@ impl Spotify { ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::User, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::User, id))?; let url = format!("me/following?type=user&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1568,12 +1528,7 @@ impl Spotify { if let Some(seed_artists) = seed_artists { let seed_artists_ids = seed_artists .iter() - .map(|id| Id::from_id_or_uri(Type::Artist, &id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, &id))?; params.insert("seed_artists".to_owned(), seed_artists_ids); } @@ -1584,12 +1539,7 @@ impl Spotify { if let Some(seed_tracks) = seed_tracks { let seed_tracks_ids = seed_tracks .iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; params.insert("seed_tracks".to_owned(), seed_tracks_ids); } @@ -1628,12 +1578,7 @@ impl Spotify { ) -> ClientResult> { let ids = tracks .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) - .fold_results(String::new(), |ids, id| ids + id.id() + ",") - .map(|mut ids| { - ids.pop(); - ids - })?; + .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; let url = format!("audio-features/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -2184,4 +2129,26 @@ mod tests { let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } + + #[test] + fn test_try_join() { + let data: Vec> = vec![Ok("a"), Ok("b"), Ok("c")]; + let joined: Result<_, u32> = data.into_iter().try_join(","); + assert_eq!("a,b,c", joined.unwrap()); + + let data: Vec> = vec![Ok("a"), Err(1), Ok("c")]; + let joined: Result<_, u32> = data.into_iter().try_join(","); + assert_eq!(1, joined.unwrap_err()); + } + + #[test] + fn test_map_try_join() { + let data: Vec<&str> = vec!["a", "b", "c"]; + let joined: Result<_, u32> = data.into_iter().map_try_join(",", Ok); + assert_eq!("a,b,c", joined.unwrap()); + + let data: Vec<&str> = vec!["a", "b", "c"]; + let joined: Result<_, u32> = data.into_iter().map_try_join(",", |_| Err::<&str, u32>(2)); + assert_eq!(2, joined.unwrap_err()); + } } diff --git a/src/model/mod.rs b/src/model/mod.rs index c50de158..c6e7e2cd 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -74,6 +74,12 @@ impl std::fmt::Display for Id<'_> { } } +impl AsRef for Id<'_> { + fn as_ref(&self) -> &str { + self.id + } +} + impl Id<'_> { pub fn _type(&self) -> Type { self._type From 24a61dc023a65a53772c4a9f85656b45baac3ab3 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 1 Dec 2020 18:01:20 +0300 Subject: [PATCH 04/59] remove itertools --- Cargo.toml | 1 - src/client.rs | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d92baee1..27594a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ ureq = { version = "1.4.1", default-features = false, features = ["json", "cooki url = "2.1.1" webbrowser = { version = "0.5.5", optional = true } strum = { version = "0.20", features = ["derive"] } -itertools = "0.9.0" [dev-dependencies] env_logger = "0.8.1" diff --git a/src/client.rs b/src/client.rs index 88cf6cb7..33137e78 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,9 +15,8 @@ use super::http::{BaseClient, Query}; use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; -use itertools::Itertools; -pub trait TryJoin: Itertools { +pub trait TryJoin: Iterator { fn try_join(&mut self, sep: &str) -> Result where Self: Iterator>, @@ -43,9 +42,23 @@ pub trait TryJoin: Itertools { { self.map(func).try_join(sep) } + + fn fold_results(&mut self, mut start: B, mut f: F) -> Result + where + Self: Iterator>, + F: FnMut(B, A) -> B, + { + for elt in self { + match elt { + Ok(v) => start = f(start, v), + Err(u) => return Err(u), + } + } + Ok(start) + } } -impl TryJoin for T where T: Itertools {} +impl TryJoin for T where T: Iterator {} /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] From e79e9bb2f1af6f973fdc22d1994c3ffe20681f96 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 1 Dec 2020 18:17:53 +0300 Subject: [PATCH 05/59] rewrite try join --- src/client.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 33137e78..a955e2b4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -40,7 +40,25 @@ pub trait TryJoin: Iterator { F: Fn(T) -> Result, R: AsRef, { - self.map(func).try_join(sep) + if let Some(item) = self.next() { + let item = func(item)?; + let value = item.as_ref(); + let (size, _) = self.size_hint(); + let cap = size * (sep.len() + value.len()); + + let mut output = String::with_capacity(cap); + output.push_str(value); + + for item in self { + let item = func(item)?; + output.push_str(sep); + output.push_str(item.as_ref()); + } + + Ok(output) + } else { + Ok(String::new()) + } } fn fold_results(&mut self, mut start: B, mut f: F) -> Result From f0d26bb6a640f55999aa54b791768e58efeeec1a Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 1 Dec 2020 23:03:58 +0300 Subject: [PATCH 06/59] cleanup --- src/client.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/client.rs b/src/client.rs index a955e2b4..ea7a94fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,23 +17,6 @@ use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; pub trait TryJoin: Iterator { - fn try_join(&mut self, sep: &str) -> Result - where - Self: Iterator>, - T: AsRef, - { - let (size, _) = self.size_hint(); - let cap = size * sep.len(); - - self.fold_results(String::with_capacity(cap), |ids, id| { - ids + id.as_ref() + sep - }) - .map(|mut ids| { - ids.pop(); - ids - }) - } - fn map_try_join(&mut self, sep: &str, func: F) -> Result where Self: Iterator, @@ -60,20 +43,6 @@ pub trait TryJoin: Iterator { Ok(String::new()) } } - - fn fold_results(&mut self, mut start: B, mut f: F) -> Result - where - Self: Iterator>, - F: FnMut(B, A) -> B, - { - for elt in self { - match elt { - Ok(v) => start = f(start, v), - Err(u) => return Err(u), - } - } - Ok(start) - } } impl TryJoin for T where T: Iterator {} From 8155bc40bd98f70f7b03442b5affb22634c45181 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 1 Dec 2020 23:38:19 +0300 Subject: [PATCH 07/59] from for iderror --- src/client.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index ea7a94fd..b6a5db5a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -86,13 +86,7 @@ pub enum ClientError { CacheFile(String), #[error("id parse error: {0:?}")] - InvalidId(IdError), -} - -impl From for ClientError { - fn from(error: IdError) -> Self { - ClientError::InvalidId(error) - } + InvalidId(#[from] IdError), } pub type ClientResult = Result; From 158eb0b2796cd7c9291d1cac4710716395977a4d Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Wed, 2 Dec 2020 06:09:31 +0300 Subject: [PATCH 08/59] fix try_join/map_try_join --- src/client.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index b6a5db5a..0fe02b32 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,14 +17,13 @@ use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; pub trait TryJoin: Iterator { - fn map_try_join(&mut self, sep: &str, func: F) -> Result + fn try_join(&mut self, sep: &str) -> Result where - Self: Iterator, - F: Fn(T) -> Result, - R: AsRef, + Self: Iterator>, + T: AsRef, { if let Some(item) = self.next() { - let item = func(item)?; + let item = item?; let value = item.as_ref(); let (size, _) = self.size_hint(); let cap = size * (sep.len() + value.len()); @@ -33,7 +32,7 @@ pub trait TryJoin: Iterator { output.push_str(value); for item in self { - let item = func(item)?; + let item = item?; output.push_str(sep); output.push_str(item.as_ref()); } @@ -43,6 +42,15 @@ pub trait TryJoin: Iterator { Ok(String::new()) } } + + fn map_try_join(&mut self, sep: &str, func: F) -> Result + where + Self: Iterator, + F: Fn(T) -> Result, + R: AsRef, + { + self.map(func).try_join(sep) + } } impl TryJoin for T where T: Iterator {} From 59eab11e6fcf37960e65088e8cde3a0b98a8edc1 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Wed, 2 Dec 2020 08:27:00 +0300 Subject: [PATCH 09/59] remove map_try_join --- src/client.rs | 71 ++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0fe02b32..67036513 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,15 +42,6 @@ pub trait TryJoin: Iterator { Ok(String::new()) } } - - fn map_try_join(&mut self, sep: &str, func: F) -> Result - where - Self: Iterator, - F: Fn(T) -> Result, - R: AsRef, - { - self.map(func).try_join(sep) - } } impl TryJoin for T where T: Iterator {} @@ -226,7 +217,8 @@ impl Spotify { // TODO: this can be improved let ids = track_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; let mut params = Query::new(); if let Some(market) = market { @@ -265,7 +257,8 @@ impl Spotify { ) -> ClientResult { let ids = artist_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .try_join(",")?; let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -379,7 +372,8 @@ impl Spotify { ) -> ClientResult { let ids = album_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .try_join(",")?; let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1046,7 +1040,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; let url = format!("me/tracks/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1067,7 +1062,8 @@ impl Spotify { ) -> ClientResult> { let ids = track_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; let url = format!("me/tracks/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1086,7 +1082,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; let url = format!("me/tracks/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1189,7 +1186,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .try_join(",")?; let url = format!("me/albums/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1209,7 +1207,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .try_join(",")?; let url = format!("me/albums/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1230,7 +1229,8 @@ impl Spotify { ) -> ClientResult> { let ids = album_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Album, id))?; + .map(|id| Id::from_id_or_uri(Type::Album, id)) + .try_join(",")?; let url = format!("me/albums/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1249,7 +1249,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .try_join(",")?; let url = format!("me/following?type=artist&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1269,7 +1270,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .try_join(",")?; let url = format!("me/following?type=artist&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1290,7 +1292,8 @@ impl Spotify { ) -> ClientResult> { let ids = artist_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, id))?; + .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .try_join(",")?; let url = format!("me/following/contains?type=artist&ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1309,7 +1312,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::User, id))?; + .map(|id| Id::from_id_or_uri(Type::User, id)) + .try_join(",")?; let url = format!("me/following?type=user&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1329,7 +1333,8 @@ impl Spotify { ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::User, id))?; + .map(|id| Id::from_id_or_uri(Type::User, id)) + .try_join(",")?; let url = format!("me/following?type=user&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1530,7 +1535,8 @@ impl Spotify { if let Some(seed_artists) = seed_artists { let seed_artists_ids = seed_artists .iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Artist, &id))?; + .map(|id| Id::from_id_or_uri(Type::Artist, &id)) + .try_join(",")?; params.insert("seed_artists".to_owned(), seed_artists_ids); } @@ -1541,7 +1547,8 @@ impl Spotify { if let Some(seed_tracks) = seed_tracks { let seed_tracks_ids = seed_tracks .iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; params.insert("seed_tracks".to_owned(), seed_tracks_ids); } @@ -1580,7 +1587,8 @@ impl Spotify { ) -> ClientResult> { let ids = tracks .into_iter() - .map_try_join(",", |id| Id::from_id_or_uri(Type::Track, id))?; + .map(|id| Id::from_id_or_uri(Type::Track, id)) + .try_join(",")?; let url = format!("audio-features/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -2142,15 +2150,4 @@ mod tests { let joined: Result<_, u32> = data.into_iter().try_join(","); assert_eq!(1, joined.unwrap_err()); } - - #[test] - fn test_map_try_join() { - let data: Vec<&str> = vec!["a", "b", "c"]; - let joined: Result<_, u32> = data.into_iter().map_try_join(",", Ok); - assert_eq!("a,b,c", joined.unwrap()); - - let data: Vec<&str> = vec!["a", "b", "c"]; - let joined: Result<_, u32> = data.into_iter().map_try_join(",", |_| Err::<&str, u32>(2)); - assert_eq!(2, joined.unwrap_err()); - } } From c4d0ef80f473fab2f7b97f18bc1ec9c88d35f679 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 3 Dec 2020 09:49:42 +0300 Subject: [PATCH 10/59] get show, shows, episodes methods use &str instead of String and validate id --- src/client.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 67036513..6e5a3470 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1964,12 +1964,12 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/shows/get-a-show/) #[maybe_async] - pub async fn get_a_show(&self, id: String, market: Option) -> ClientResult { + pub async fn get_a_show(&self, id: &str, market: Option) -> ClientResult { let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!("shows/{}", id); + let url = format!("shows/{}", Id::from_id_or_uri(Type::Show, id)?.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2016,7 +2016,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: String, + id: &str, limit: L, offset: O, market: Option, @@ -2027,7 +2027,10 @@ impl Spotify { if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!("shows/{}/episodes", id); + let url = format!( + "shows/{}/episodes", + Id::from_id_or_uri(Type::Show, id)?.id() + ); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2044,10 +2047,10 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: String, + id: &str, market: Option, ) -> ClientResult { - let url = format!("episodes/{}", id); + let url = format!("episodes/{}", Id::from_id_or_uri(Type::Episode, id)?.id()); let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); From b74787e5124487f82f8cdd1114196dfb1654c5f8 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 3 Dec 2020 09:57:33 +0300 Subject: [PATCH 11/59] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f85f736..70b7ed7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Remove `itertools` dependency by using the standard library. + Remove `rand` in place of `getrandom` to [reduce total dependencies and compile times](https://github.com/ramsayleung/rspotify/issues/108#issuecomment-673587185). + Cleanup, reduced repetitive code and boilerplate internally in several places ([#117](https://github.com/ramsayleung/rspotify/pull/117), [#113](https://github.com/ramsayleung/rspotify/pull/113), [#107](https://github.com/ramsayleung/rspotify/pull/107), [#106](https://github.com/ramsayleung/rspotify/pull/106)). + + Added internal zero-copy type for Spotify ids, reduced number of allocations/clones ([#161](https://github.com/ramsayleung/rspotify/pull/161)). + Updated dependencies to the latest versions, integrated Dependabot to keep track of them ([#105](https://github.com/ramsayleung/rspotify/pull/105), [#111](https://github.com/ramsayleung/rspotify/pull/111)). - ([#145](https://github.com/ramsayleung/rspotify/pull/145)) Mark `SimplifiedEpisode.language` as deprecated. - ([#145](https://github.com/ramsayleung/rspotify/pull/145)) Derive `PartialEq` and `Eq` for models: @@ -131,6 +132,10 @@ If we missed any change or there's something you'd like to discuss about this ve - `user_follow_users` - `user_unfollow_users` - `audios_features` + + The endpoints which changes parameter from `String` to `&str`: + - `get_a_show` + - `get_an_episode` + - `get_shows_episodes` - ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Rename endpoints with more fitting name: + `audio_analysis` -> `track_analysis` + `audio_features` -> `track_features` @@ -159,6 +164,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Change `{FullArtist, FullPlaylist, PublicUser, PrivateUser}::followers` from `HashMap>` to struct `Followers` + Replace `Actions::disallows` with a `Vec` by removing all entires whose value is false, which will result in a simpler API + Replace `{FullAlbum, SimplifiedEpisode, FullEpisode}::release_date_precision` from `String` to `DatePrecision` enum, makes it easier to use. + + Added more strict validation for URI and id parameters everywhere, new error variant `ClientError::InvalidId(IdError)` for invalid input ids and URIs. ## 0.10 (2020/07/01) From 3672eb3d3a98cb1509f953e0515ee741946e2e00 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 17 Dec 2020 14:05:47 +0300 Subject: [PATCH 12/59] use Display instead of AsRefStr for Type, FromStr for Type --- src/model/enums/types.rs | 4 ++-- src/model/mod.rs | 20 +++----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index 0e7e9a76..f090555d 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, ToString}; +use strum::{Display, ToString, EnumString}; /// Copyright type: `C` = the copyright, `P` = the sound recording (performance) copyright. /// @@ -28,7 +28,7 @@ pub enum AlbumType { } /// Type: `artist`, `album`, `track`, `playlist`, `show` or `episode` -#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, ToString, AsRefStr)] +#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, Display, EnumString)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum Type { diff --git a/src/model/mod.rs b/src/model/mod.rs index c6e7e2cd..ea8bac64 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -66,11 +66,7 @@ pub enum IdError { impl std::fmt::Display for Id<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("spotify:")?; - f.write_str(self._type.as_ref())?; - f.write_str(":")?; - f.write_str(self.id)?; - Ok(()) + write!(f, "spotify:{}:{}", self._type, self.id) } } @@ -90,7 +86,7 @@ impl Id<'_> { } pub fn uri(&self) -> String { - format!("spotify:{}:{}", self._type.as_ref(), self.id) + format!("spotify:{}:{}", self._type, self.id) } pub fn from_id_or_uri<'a, 'b: 'a>(_type: Type, id_or_uri: &'b str) -> Result, IdError> { @@ -118,17 +114,7 @@ impl Id<'_> { } else { return Err(IdError::InvalidPrefix); } { - let _type = match tpe { - "artist" => Type::Artist, - "album" => Type::Album, - "track" => Type::Track, - "user" => Type::User, - "playlist" => Type::Playlist, - "show" => Type::Show, - "episode" => Type::Episode, - _ => return Err(IdError::InvalidType), - }; - + let _type = tpe.parse().map_err(|_| IdError::InvalidType)?; Self::from_id(_type, &id[1..]) } else { Err(IdError::InvalidFormat) From a21d3bba6b7ceaddc6ebc3d6810b99800f68ac4f Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 17 Dec 2020 18:06:32 +0300 Subject: [PATCH 13/59] rewrite from_uri() --- src/model/enums/types.rs | 2 +- src/model/mod.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index f090555d..da5773e6 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum::{Display, ToString, EnumString}; +use strum::{Display, EnumString, ToString}; /// Copyright type: `C` = the copyright, `P` = the sound recording (performance) copyright. /// diff --git a/src/model/mod.rs b/src/model/mod.rs index ea8bac64..96468ffe 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -107,13 +107,14 @@ impl Id<'_> { } pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { - if let Some((tpe, id)) = if let Some(uri) = uri.strip_prefix("spotify:") { - uri.rfind(':').map(|mid| uri.split_at(mid)) - } else if let Some(uri) = uri.strip_prefix("spotify/") { - uri.rfind('/').map(|mid| uri.split_at(mid)) - } else { - return Err(IdError::InvalidPrefix); - } { + let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; + let sep = match rest.chars().next() { + Some(ch) if ch == '/' || ch == ':' => ch, + _ => return Err(IdError::InvalidPrefix), + }; + let rest = &rest[1..]; + + if let Some((tpe, id)) = rest.rfind(sep).map(|mid| rest.split_at(mid)) { let _type = tpe.parse().map_err(|_| IdError::InvalidType)?; Self::from_id(_type, &id[1..]) } else { From 88ab887252be5c54291d3988c16e7d6fe6dc16cc Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 17 Dec 2020 18:44:46 +0300 Subject: [PATCH 14/59] add Id type docs --- src/model/mod.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/model/mod.rs b/src/model/mod.rs index 96468ffe..14a38a46 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -50,6 +50,7 @@ pub enum PlayingItem { Episode(show::FullEpisode), } +/// A Spotify object id of given [type](crate::model::enums::types::Type) #[derive(Debug, PartialEq, Eq, Clone)] pub struct Id<'id> { _type: Type, @@ -77,18 +78,48 @@ impl AsRef for Id<'_> { } impl Id<'_> { + /// Spotify object type pub fn _type(&self) -> Type { self._type } + /// Spotify object id (guaranteed to be a string of alphanumeric characters) pub fn id(&self) -> &str { &self.id } + /// Spotify object URI in a well-known format: spotify:type:id + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. pub fn uri(&self) -> String { format!("spotify:{}:{}", self._type, self.id) } + /// Full Spotify object URL, can be opened in a browser + /// + /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI + pub fn url(&self) -> String { + format!("https://open.spotify.com/{}/{}", self._type, self.id) + } + + /// 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 alphanumeric string. + /// The URI must be of given `_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 alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part is not equal to `_type`, + /// - `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 non-alphanumeric characters), + /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. pub fn from_id_or_uri<'a, 'b: 'a>(_type: Type, id_or_uri: &'b str) -> Result, IdError> { match Self::from_uri(id_or_uri) { Ok(id) if id._type == _type => Ok(id), @@ -98,6 +129,13 @@ impl Id<'_> { } } + /// Parse Spotify id from string slice + /// + /// A valid Spotify object id must be a non-empty alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. pub fn from_id<'a, 'b: 'a>(_type: Type, id: &'b str) -> Result, IdError> { if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { Ok(Id { _type, id }) @@ -106,6 +144,20 @@ impl Id<'_> { } } + /// 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 alphanumeric 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, + /// - `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. pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; let sep = match rest.chars().next() { From 3c3f3b27df825288723391252a12c138d778dafb Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 17 Dec 2020 19:18:27 +0300 Subject: [PATCH 15/59] use Id type for arguments (TOOD: tests) --- src/client.rs | 216 ++++++++++++++++++++++++----------------------- src/model/mod.rs | 8 ++ 2 files changed, 119 insertions(+), 105 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6e5a3470..b2a75746 100644 --- a/src/client.rs +++ b/src/client.rs @@ -194,9 +194,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-track/) #[maybe_async] - pub async fn track(&self, track_id: &str) -> ClientResult { - let trid = Id::from_id_or_uri(Type::Track, track_id)?; - let url = format!("tracks/{}", trid.id()); + pub async fn track(&self, track_id: Id<'_>) -> ClientResult { + let url = format!("tracks/{}", track_id.check_type(Type::Track)?.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -211,13 +210,12 @@ impl Spotify { #[maybe_async] pub async fn tracks<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator>, market: Option, ) -> ClientResult { - // TODO: this can be improved let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) + .map(|id| id.check_type(Type::Track)) .try_join(",")?; let mut params = Query::new(); @@ -237,9 +235,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-artist/) #[maybe_async] - pub async fn artist(&self, artist_id: &str) -> ClientResult { - let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; - let url = format!("artists/{}", trid.id()); + pub async fn artist(&self, artist_id: Id<'_>) -> ClientResult { + let url = format!("artists/{}", artist_id.check_type(Type::Artist)?.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -253,11 +250,11 @@ impl Spotify { #[maybe_async] pub async fn artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator>, ) -> ClientResult { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .map(|id| id.check_type(Type::Artist)) .try_join(",")?; let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -277,7 +274,7 @@ impl Spotify { #[maybe_async] pub async fn artist_albums( &self, - artist_id: &str, + artist_id: Id<'_>, album_type: Option, country: Option, limit: Option, @@ -296,8 +293,10 @@ impl Spotify { if let Some(country) = country { params.insert("country".to_owned(), country.to_string()); } - let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; - let url = format!("artists/{}/albums", trid.id()); + let url = format!( + "artists/{}/albums", + artist_id.check_type(Type::Artist)?.id() + ); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -313,7 +312,7 @@ impl Spotify { #[maybe_async] pub async fn artist_top_tracks>>( &self, - artist_id: &str, + artist_id: Id<'_>, country: T, ) -> ClientResult { let mut params = Query::with_capacity(1); @@ -322,8 +321,10 @@ impl Spotify { country.into().unwrap_or(Country::UnitedStates).to_string(), ); - let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; - let url = format!("artists/{}/top-tracks", trid.id()); + let url = format!( + "artists/{}/top-tracks", + artist_id.check_type(Type::Artist)?.id() + ); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -337,9 +338,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-related-artists/) #[maybe_async] - pub async fn artist_related_artists(&self, artist_id: &str) -> ClientResult { - let trid = Id::from_id_or_uri(Type::Artist, artist_id)?; - let url = format!("artists/{}/related-artists", trid.id()); + pub async fn artist_related_artists(&self, artist_id: Id<'_>) -> ClientResult { + let url = format!( + "artists/{}/related-artists", + artist_id.check_type(Type::Artist)?.id() + ); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -351,9 +354,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-album/) #[maybe_async] - pub async fn album(&self, album_id: &str) -> ClientResult { - let trid = Id::from_id_or_uri(Type::Album, album_id)?; - let url = format!("albums/{}", trid.id()); + pub async fn album(&self, album_id: Id<'_>) -> ClientResult { + let url = format!("albums/{}", album_id.check_type(Type::Album)?.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -368,11 +370,11 @@ impl Spotify { #[maybe_async] pub async fn albums<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator>, ) -> ClientResult { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) + .map(|id| id.check_type(Type::Album)) .try_join(",")?; let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -431,15 +433,14 @@ impl Spotify { #[maybe_async] pub async fn album_track>, O: Into>>( &self, - album_id: &str, + album_id: Id<'_>, limit: L, offset: O, ) -> ClientResult> { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let trid = Id::from_id_or_uri(Type::Album, album_id)?; - let url = format!("albums/{}/tracks", trid.id()); + let url = format!("albums/{}/tracks", album_id.check_type(Type::Album)?.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -467,7 +468,7 @@ impl Spotify { #[maybe_async] pub async fn playlist( &self, - playlist_id: &str, + playlist_id: Id<'_>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -479,8 +480,7 @@ impl Spotify { params.insert("market".to_owned(), market.to_string()); } - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let url = format!("playlists/{}", plid.id()); + let url = format!("playlists/{}", playlist_id.check_type(Type::Playlist)?.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -541,7 +541,7 @@ impl Spotify { pub async fn user_playlist( &self, user_id: &str, - playlist_id: Option<&str>, + playlist_id: Option>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -554,8 +554,11 @@ impl Spotify { } match playlist_id { Some(playlist_id) => { - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let url = format!("users/{}/playlists/{}", user_id, plid.id()); + let url = format!( + "users/{}/playlists/{}", + user_id, + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -580,7 +583,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_tracks>, O: Into>>( &self, - playlist_id: &str, + playlist_id: Id<'_>, fields: Option<&str>, limit: L, offset: O, @@ -595,8 +598,10 @@ impl Spotify { if let Some(fields) = fields { params.insert("fields".to_owned(), fields.to_owned()); } - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -689,22 +694,23 @@ impl Spotify { #[maybe_async] pub async fn playlist_add_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: Id<'_>, + track_ids: impl IntoIterator>, position: Option, ) -> ClientResult { - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let uris = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .map(|id| id.check_type(Type::Track).map(|id| id.uri())) .collect::, _>>()?; let mut params = json!({ "uris": uris }); if let Some(position) = position { json_insert!(params, "position", position); } - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.post(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -720,17 +726,19 @@ impl Spotify { #[maybe_async] pub async fn playlist_replace_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: Id<'_>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; let uris = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .map(|id| id.check_type(Type::Playlist).map(|id| id.uri())) .collect::, _>>()?; let params = json!({ "uris": uris }); - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); self.put(&url, None, ¶ms).await?; Ok(()) @@ -749,13 +757,12 @@ impl Spotify { #[maybe_async] pub async fn playlist_reorder_tracks>>( &self, - playlist_id: &str, + playlist_id: Id<'_>, range_start: i32, range_length: R, insert_before: i32, snapshot_id: Option, ) -> ClientResult { - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; let mut params = json! ({ "range_start": range_start, "range_length": range_length.into().unwrap_or(1), @@ -765,7 +772,10 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.put(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -781,15 +791,13 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: Id<'_>, + track_ids: impl IntoIterator>, snapshot_id: Option, ) -> ClientResult { - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let tracks = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id).map(|id| id.uri())) + .map(|id| id.check_type(Type::Track).map(|id| id.uri())) .collect::, _>>()?; let mut params = json!({ "tracks": tracks.into_iter().map(|uri| { @@ -803,7 +811,10 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -840,13 +851,10 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_specific_occurrences_of_tracks( &self, - playlist_id: &str, + playlist_id: Id<'_>, tracks: Vec>, snapshot_id: Option, ) -> ClientResult { - // TODO: this can be improved - let plid = Id::from_id_or_uri(Type::Playlist, playlist_id)?; - let mut ftracks: Vec> = vec![]; for track in tracks { let mut map = Map::new(); @@ -865,7 +873,10 @@ impl Spotify { if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid.id()); + let url = format!( + "playlists/{}/tracks", + playlist_id.check_type(Type::Playlist)?.id() + ); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -1036,11 +1047,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) + .map(|id| id.check_type(Type::Track)) .try_join(",")?; let url = format!("me/tracks/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1058,11 +1069,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator>, ) -> ClientResult> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) + .map(|id| id.check_type(Type::Track)) .try_join(",")?; let url = format!("me/tracks/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -1078,11 +1089,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = track_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) + .map(|id| id.check_type(Type::Track)) .try_join(",")?; let url = format!("me/tracks/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1182,11 +1193,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) + .map(|id| id.check_type(Type::Album)) .try_join(",")?; let url = format!("me/albums/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1203,11 +1214,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) + .map(|id| id.check_type(Type::Album)) .try_join(",")?; let url = format!("me/albums/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1225,11 +1236,11 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator>, ) -> ClientResult> { let ids = album_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Album, id)) + .map(|id| id.check_type(Type::Album)) .try_join(",")?; let url = format!("me/albums/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -1245,11 +1256,11 @@ impl Spotify { #[maybe_async] pub async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .map(|id| id.check_type(Type::Artist)) .try_join(",")?; let url = format!("me/following?type=artist&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1266,11 +1277,11 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .map(|id| id.check_type(Type::Artist)) .try_join(",")?; let url = format!("me/following?type=artist&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1288,11 +1299,11 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator>, ) -> ClientResult> { let ids = artist_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::Artist, id)) + .map(|id| id.check_type(Type::Artist)) .try_join(",")?; let url = format!("me/following/contains?type=artist&ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -1308,11 +1319,11 @@ impl Spotify { #[maybe_async] pub async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::User, id)) + .map(|id| id.check_type(Type::User)) .try_join(",")?; let url = format!("me/following?type=user&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1329,11 +1340,11 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = user_ids .into_iter() - .map(|id| Id::from_id_or_uri(Type::User, id)) + .map(|id| id.check_type(Type::User)) .try_join(",")?; let url = format!("me/following?type=user&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1494,9 +1505,9 @@ impl Spotify { #[maybe_async] pub async fn recommendations>>( &self, - seed_artists: Option>, + seed_artists: Option>>, seed_genres: Option>, - seed_tracks: Option>, + seed_tracks: Option>>, limit: L, country: Option, payload: &Map, @@ -1534,8 +1545,8 @@ impl Spotify { if let Some(seed_artists) = seed_artists { let seed_artists_ids = seed_artists - .iter() - .map(|id| Id::from_id_or_uri(Type::Artist, &id)) + .into_iter() + .map(|id| id.check_type(Type::Artist)) .try_join(",")?; params.insert("seed_artists".to_owned(), seed_artists_ids); } @@ -1546,8 +1557,8 @@ impl Spotify { if let Some(seed_tracks) = seed_tracks { let seed_tracks_ids = seed_tracks - .iter() - .map(|id| Id::from_id_or_uri(Type::Track, id)) + .into_iter() + .map(|id| id.check_type(Type::Track)) .try_join(",")?; params.insert("seed_tracks".to_owned(), seed_tracks_ids); } @@ -1567,9 +1578,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-features/) #[maybe_async] - pub async fn track_features(&self, track: &str) -> ClientResult { - let track_id = Id::from_id_or_uri(Type::Track, track)?; - let url = format!("audio-features/{}", track_id.id()); + pub async fn track_features(&self, track_id: Id<'_>) -> ClientResult { + let url = format!("audio-features/{}", track_id.check_type(Type::Track)?.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1606,9 +1616,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-analysis/) #[maybe_async] - pub async fn track_analysis(&self, track: &str) -> ClientResult { - let trid = Id::from_id_or_uri(Type::Track, track)?; - let url = format!("audio-analysis/{}", trid.id()); + pub async fn track_analysis(&self, track_id: Id<'_>) -> ClientResult { + let url = format!("audio-analysis/{}", track_id.check_type(Type::Track)?.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1964,12 +1973,12 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/shows/get-a-show/) #[maybe_async] - pub async fn get_a_show(&self, id: &str, market: Option) -> ClientResult { + pub async fn get_a_show(&self, id: Id<'_>, market: Option) -> ClientResult { let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!("shows/{}", Id::from_id_or_uri(Type::Show, id)?.id()); + let url = format!("shows/{}", id.check_type(Type::Show)?.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2016,7 +2025,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: &str, + id: Id<'_>, limit: L, offset: O, market: Option, @@ -2027,10 +2036,7 @@ impl Spotify { if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!( - "shows/{}/episodes", - Id::from_id_or_uri(Type::Show, id)?.id() - ); + let url = format!("shows/{}/episodes", id.check_type(Type::Show)?.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2047,10 +2053,10 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: &str, + id: Id<'_>, market: Option, ) -> ClientResult { - let url = format!("episodes/{}", Id::from_id_or_uri(Type::Episode, id)?.id()); + let url = format!("episodes/{}", id.check_type(Type::Episode)?.id()); let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); diff --git a/src/model/mod.rs b/src/model/mod.rs index 14a38a46..f758c9d3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -83,6 +83,14 @@ impl Id<'_> { self._type } + pub(in crate) fn check_type(self, _type: Type) -> Result { + if self._type == _type { + Ok(self) + } else { + Err(IdError::InvalidType) + } + } + /// Spotify object id (guaranteed to be a string of alphanumeric characters) pub fn id(&self) -> &str { &self.id From 2d62cc24711d9eee3915975d99b606c9bc37bfdd Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 17 Dec 2020 19:32:44 +0300 Subject: [PATCH 16/59] update tests --- tests/test_with_credential.rs | 45 ++++++++++++---------- tests/test_with_oauth.rs | 70 +++++++++++++++++------------------ 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 28086ec1..863560da 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -2,7 +2,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; -use rspotify::model::{AlbumType, Country}; +use rspotify::model::{AlbumType, Country, Id, Type}; use rspotify::oauth2::CredentialsBuilder; use maybe_async::maybe_async; @@ -27,16 +27,16 @@ pub async fn creds_client() -> Spotify { #[maybe_async] #[maybe_async_test] async fn test_album() { - let birdy_uri = "spotify:album:0sNOF9WDwhWunNAHPD3Baj"; + let birdy_uri = Id::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); creds_client().await.album(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_albums() { - let birdy_uri1 = "spotify:album:41MnTivkwTO3UUJ8DrqEJJ"; - let birdy_uri2 = "spotify:album:6JWc4iAiJ9FjyK0B59ABb4"; - let birdy_uri3 = "spotify:album:6UXCm6bOO4gFlDQZV5yL37"; + let birdy_uri1 = Id::from_uri("spotify:album:41MnTivkwTO3UUJ8DrqEJJ").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:album:6JWc4iAiJ9FjyK0B59ABb4").unwrap(); + let birdy_uri3 = Id::from_uri("spotify:album:6UXCm6bOO4gFlDQZV5yL37").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2, birdy_uri3]; creds_client().await.albums(track_uris).await.unwrap(); } @@ -44,7 +44,7 @@ async fn test_albums() { #[maybe_async] #[maybe_async_test] async fn test_album_tracks() { - let birdy_uri = "spotify:album:6akEvsycLGftJxYudPjmqK"; + let birdy_uri = Id::from_uri("spotify:album:6akEvsycLGftJxYudPjmqK").unwrap(); creds_client() .await .album_track(birdy_uri, Some(2), None) @@ -55,7 +55,7 @@ async fn test_album_tracks() { #[maybe_async] #[maybe_async_test] async fn test_artist_related_artists() { - let birdy_uri = "spotify:artist:43ZHCT0cAZBISjO8DG9PnE"; + let birdy_uri = Id::from_uri("spotify:artist:43ZHCT0cAZBISjO8DG9PnE").unwrap(); creds_client() .await .artist_related_artists(birdy_uri) @@ -66,14 +66,14 @@ async fn test_artist_related_artists() { #[maybe_async] #[maybe_async_test] async fn test_artist() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client().await.artist(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_artists_albums() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client() .await .artist_albums( @@ -90,8 +90,8 @@ async fn test_artists_albums() { #[maybe_async] #[maybe_async_test] async fn test_artists() { - let birdy_uri1 = "spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy"; - let birdy_uri2 = "spotify:artist:3dBVyJ7JuOMt4GE9607Qin"; + let birdy_uri1 = Id::from_uri("spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:artist:3dBVyJ7JuOMt4GE9607Qin").unwrap(); let artist_uris = vec![birdy_uri1, birdy_uri2]; creds_client().await.artists(artist_uris).await.unwrap(); } @@ -99,7 +99,7 @@ async fn test_artists() { #[maybe_async] #[maybe_async_test] async fn test_artist_top_tracks() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client() .await .artist_top_tracks(birdy_uri, Country::UnitedStates) @@ -110,14 +110,14 @@ async fn test_artist_top_tracks() { #[maybe_async] #[maybe_async_test] async fn test_audio_analysis() { - let track = "06AKEBrKUckW0KREUWRnvT"; + let track = Id::from_id(Type::Track, "06AKEBrKUckW0KREUWRnvT").unwrap(); creds_client().await.track_analysis(track).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_audio_features() { - let track = "spotify:track:06AKEBrKUckW0KREUWRnvT"; + let track = Id::from_uri("spotify:track:06AKEBrKUckW0KREUWRnvT").unwrap(); creds_client().await.track_features(track).await.unwrap(); } @@ -146,15 +146,15 @@ async fn test_user() { #[maybe_async] #[maybe_async_test] async fn test_track() { - let birdy_uri = "spotify:track:6rqhFgbbKwnb9MLmUQDhG6"; + let birdy_uri = Id::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); creds_client().await.track(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_tracks() { - let birdy_uri1 = "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp"; - let birdy_uri2 = "spotify:track:3twNvmDtFQtAd5gMKedhLD"; + let birdy_uri1 = Id::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2]; creds_client().await.tracks(track_uris, None).await.unwrap(); } @@ -164,7 +164,11 @@ async fn test_tracks() { async fn test_existing_playlist() { creds_client() .await - .playlist("37i9dQZF1DZ06evO45P0Eo", None, None) + .playlist( + Id::from_id(Type::Playlist, "37i9dQZF1DZ06evO45P0Eo").unwrap(), + None, + None, + ) .await .unwrap(); } @@ -172,6 +176,9 @@ async fn test_existing_playlist() { #[maybe_async] #[maybe_async_test] async fn test_fake_playlist() { - let playlist = creds_client().await.playlist("fake_id", None, None).await; + let playlist = creds_client() + .await + .playlist(Id::from_id(Type::Playlist, "fakeid").unwrap(), None, None) + .await; assert!(!playlist.is_ok()); } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index a09b67dc..eb1fd472 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -19,7 +19,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; -use rspotify::model::{Country, RepeatState, SearchType, TimeRange}; +use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange, Type}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use std::env; @@ -170,8 +170,8 @@ async fn test_current_user_saved_albums_add() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(album_id2); - album_ids.push(album_id1); + album_ids.push(Id::from_id(Type::Album, album_id2).unwrap()); + album_ids.push(Id::from_id(Type::Album, album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_add(album_ids) @@ -186,8 +186,8 @@ async fn test_current_user_saved_albums_delete() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(album_id2); - album_ids.push(album_id1); + album_ids.push(Id::from_id(Type::Album, album_id2).unwrap()); + album_ids.push(Id::from_id(Type::Album, album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_delete(album_ids) @@ -213,8 +213,8 @@ async fn test_current_user_saved_tracks_add() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_add(tracks_ids) @@ -229,8 +229,8 @@ async fn test_current_user_saved_tracks_contains() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_contains(tracks_ids) @@ -245,8 +245,8 @@ async fn test_current_user_saved_tracks_delete() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_delete(tracks_ids) @@ -365,8 +365,8 @@ async fn test_previous_playback() { #[ignore] async fn test_recommendations() { let mut payload = Map::new(); - let seed_artists = vec!["4NHQUGzhtTLFvgF5SZesLK".to_owned()]; - let seed_tracks = vec!["0c6xIDDpzE81m2q797ordA".to_owned()]; + let seed_artists = vec![Id::from_id(Type::Artist, "4NHQUGzhtTLFvgF5SZesLK").unwrap()]; + let seed_tracks = vec![Id::from_id(Type::Track, "0c6xIDDpzE81m2q797ordA").unwrap()]; payload.insert("min_energy".to_owned(), 0.4.into()); payload.insert("min_popularity".to_owned(), 50.into()); oauth_client() @@ -509,8 +509,8 @@ async fn test_user_follow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(artist_id2); - artists.push(artist_id1); + artists.push(Id::from_id(Type::Artist, artist_id2).unwrap()); + artists.push(Id::from_id(Type::Artist, artist_id1).unwrap()); oauth_client() .await .user_follow_artists(artists) @@ -525,8 +525,8 @@ async fn test_user_unfollow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(artist_id2); - artists.push(artist_id1); + artists.push(Id::from_id(Type::Artist, artist_id2).unwrap()); + artists.push(Id::from_id(Type::Artist, artist_id1).unwrap()); oauth_client() .await .user_unfollow_artists(artists) @@ -539,7 +539,7 @@ async fn test_user_unfollow_artist() { #[ignore] async fn test_user_follow_users() { let mut users = vec![]; - let user_id1 = "exampleuser01"; + let user_id1 = Id::from_id(Type::User, "exampleuser01").unwrap(); users.push(user_id1); oauth_client().await.user_follow_users(users).await.unwrap(); } @@ -549,7 +549,7 @@ async fn test_user_follow_users() { #[ignore] async fn test_user_unfollow_users() { let mut users = vec![]; - let user_id1 = "exampleuser01"; + let user_id1 = Id::from_id(Type::User, "exampleuser01").unwrap(); users.push(user_id1); oauth_client() .await @@ -562,11 +562,11 @@ async fn test_user_unfollow_users() { #[maybe_async_test] #[ignore] async fn test_playlist_add_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); tracks_ids.push(track_id1); - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); oauth_client() .await @@ -634,7 +634,7 @@ async fn test_playlist_follow_playlist() { #[maybe_async_test] #[ignore] async fn test_playlist_recorder_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let range_start = 0; let insert_before = 1; let range_length = 1; @@ -649,10 +649,10 @@ async fn test_playlist_recorder_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_all_occurrences_of_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); tracks_ids.push(track_id1); oauth_client() @@ -666,7 +666,7 @@ async fn test_playlist_remove_all_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_specific_occurrences_of_tracks() { - let playlist_id = String::from("5jAOgWXCBKuinsGiZxjDQ5"); + let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks = vec![]; let mut map1 = Map::new(); let mut position1 = vec![]; @@ -689,7 +689,7 @@ async fn test_playlist_remove_specific_occurrences_of_tracks() { tracks.push(map2); oauth_client() .await - .playlist_remove_specific_occurrences_of_tracks(&playlist_id, tracks, None) + .playlist_remove_specific_occurrences_of_tracks(playlist_id, tracks, None) .await .unwrap(); } @@ -698,10 +698,10 @@ async fn test_playlist_remove_specific_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_replace_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); tracks_ids.push(track_id1); oauth_client() @@ -716,10 +716,10 @@ async fn test_playlist_replace_tracks() { #[ignore] async fn test_user_playlist() { let user_id = "spotify"; - let mut playlist_id = String::from("59ZbFPES4DQwEjBpWHzrtC"); + let mut playlist_id = Id::from_id(Type::Playlist, "59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await - .user_playlist(user_id, Some(&mut playlist_id), None, None) + .user_playlist(user_id, Some(playlist_id), None, None) .await .unwrap(); } @@ -740,10 +740,10 @@ async fn test_user_playlists() { #[maybe_async_test] #[ignore] async fn test_playlist_tracks() { - let playlist_id = String::from("spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"); + let playlist_id = Id::from_uri("spotify:playlist:59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await - .playlist_tracks(&playlist_id, None, Some(2), None, None) + .playlist_tracks(playlist_id, None, Some(2), None, None) .await .unwrap(); } From 9f2bb106c4e3e875a87b3415a35e54235a3dccd1 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 18 Dec 2020 01:11:48 +0300 Subject: [PATCH 17/59] fix lifetimes --- src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2522d6dc..95a78e03 100644 --- a/src/client.rs +++ b/src/client.rs @@ -698,7 +698,7 @@ impl Spotify { pub async fn playlist_add_tracks<'a>( &self, playlist_id: Id<'_>, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, position: Option, ) -> ClientResult { let uris = track_ids @@ -730,7 +730,7 @@ impl Spotify { pub async fn playlist_replace_tracks<'a>( &self, playlist_id: Id<'_>, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let uris = track_ids .into_iter() @@ -1051,7 +1051,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = track_ids .into_iter() From 5cfdcc5842512109ac117c20aa6222cc13c0bcd7 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 18 Dec 2020 01:19:25 +0300 Subject: [PATCH 18/59] update examples --- examples/album.rs | 3 ++- examples/track.rs | 3 ++- examples/tracks.rs | 5 +++-- examples/with_refresh_token.rs | 7 ++++--- tests/test_with_oauth.rs | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/album.rs b/examples/album.rs index 8331f1f3..f2213185 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -34,7 +35,7 @@ async fn main() { spotify.request_client_token().await.unwrap(); // Running the requests - let birdy_uri = "spotify:album:0sNOF9WDwhWunNAHPD3Baj"; + let birdy_uri = Id::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); let albums = spotify.album(birdy_uri).await; println!("Response: {:#?}", albums); diff --git a/examples/track.rs b/examples/track.rs index 8db653c2..66446771 100644 --- a/examples/track.rs +++ b/examples/track.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -34,7 +35,7 @@ async fn main() { spotify.request_client_token().await.unwrap(); // Running the requests - let birdy_uri = "spotify:track:6rqhFgbbKwnb9MLmUQDhG6"; + let birdy_uri = Id::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); let track = spotify.track(birdy_uri).await; println!("Response: {:#?}", track); diff --git a/examples/tracks.rs b/examples/tracks.rs index 2a2658c1..7549ecd7 100644 --- a/examples/tracks.rs +++ b/examples/tracks.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -33,8 +34,8 @@ async fn main() { // so `...` is used instead of `prompt_for_user_token`. spotify.request_client_token().await.unwrap(); - let birdy_uri1 = "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp"; - let birdy_uri2 = "spotify:track:3twNvmDtFQtAd5gMKedhLD"; + let birdy_uri1 = Id::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2]; let tracks = spotify.tracks(track_uris, None).await; println!("Response: {:?}", tracks); diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 1537d3f8..5f77177c 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -16,15 +16,16 @@ //! so in the case of Spotify it doesn't seem to revoke them at all. use rspotify::client::{Spotify, SpotifyBuilder}; +use rspotify::model::{Id, Type}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. async fn do_things(spotify: Spotify) { let artists = vec![ - "3RGLhK1IP9jnYFH4BRFJBS", // The Clash - "0yNLKJebCb8Aueb54LYya3", // New Order - "2jzc5TC5TVFLXQlBNiIUzE", // a-ha + Id::from_id(Type::Artist, "3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash + Id::from_id(Type::Artist, "0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order + Id::from_id(Type::Artist, "2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha ]; spotify .user_follow_artists(artists.clone()) diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index f812e1e5..25953b08 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -716,7 +716,7 @@ async fn test_playlist_replace_tracks() { #[ignore] async fn test_user_playlist() { let user_id = "spotify"; - let mut playlist_id = Id::from_id(Type::Playlist, "59ZbFPES4DQwEjBpWHzrtC").unwrap(); + let playlist_id = Id::from_id(Type::Playlist, "59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await .user_playlist(user_id, Some(playlist_id), None, None) From 9967f3548a2e0d2c32786a61d0883c52284d6309 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 18 Dec 2020 01:26:40 +0300 Subject: [PATCH 19/59] add IdError docs --- src/model/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/model/mod.rs b/src/model/mod.rs index 95a7dc26..56b8eee0 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -177,11 +177,19 @@ pub struct Id<'id> { id: &'id str, } +/// Spotify id or URI parsing error +/// +/// See also [`Id`](crate::model::Id) for details. #[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] pub enum IdError { + /// 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) 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) InvalidType, + /// Spotify id is invalid (empty or contains non-alphanumeric characters) InvalidId, } From afe67f7d47ec538b56a906230e338036b000cd73 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 14:59:46 +0300 Subject: [PATCH 20/59] add idbuf owning type, use type-safe id parsing --- examples/with_refresh_token.rs | 8 +- src/client.rs | 298 ++++++++++++--------------------- src/model/enums/types.rs | 45 +++++ src/model/mod.rs | 113 +++++++++---- tests/test_with_credential.rs | 14 +- tests/test_with_oauth.rs | 38 ++--- 6 files changed, 256 insertions(+), 260 deletions(-) diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 5f77177c..3e7d77ca 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -16,16 +16,16 @@ //! so in the case of Spotify it doesn't seem to revoke them at all. use rspotify::client::{Spotify, SpotifyBuilder}; -use rspotify::model::{Id, Type}; +use rspotify::model::Id; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. async fn do_things(spotify: Spotify) { let artists = vec![ - Id::from_id(Type::Artist, "3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash - Id::from_id(Type::Artist, "0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order - Id::from_id(Type::Artist, "2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha + Id::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash + Id::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order + Id::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha ]; spotify .user_follow_artists(artists.clone()) diff --git a/src/client.rs b/src/client.rs index 95a78e03..302b3175 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,14 +16,13 @@ use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; -pub trait TryJoin: Iterator { - fn try_join(&mut self, sep: &str) -> Result +pub trait Join: Iterator { + fn join(&mut self, sep: &str) -> String where - Self: Iterator>, - T: AsRef, + Self: Iterator, + Self::Item: AsRef, { if let Some(item) = self.next() { - let item = item?; let value = item.as_ref(); let (size, _) = self.size_hint(); let cap = size * (sep.len() + value.len()); @@ -32,19 +31,18 @@ pub trait TryJoin: Iterator { output.push_str(value); for item in self { - let item = item?; output.push_str(sep); output.push_str(item.as_ref()); } - Ok(output) + output } else { - Ok(String::new()) + String::new() } } } -impl TryJoin for T where T: Iterator {} +impl Join for T where T: Iterator {} /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] @@ -194,8 +192,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-track/) #[maybe_async] - pub async fn track(&self, track_id: Id<'_>) -> ClientResult { - let url = format!("tracks/{}", track_id.check_type(Type::Track)?.id()); + pub async fn track(&self, track_id: Id<'_, idtypes::Track>) -> ClientResult { + let url = format!("tracks/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -210,13 +208,10 @@ impl Spotify { #[maybe_async] pub async fn tracks<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, market: Option, ) -> ClientResult> { - let ids = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let ids = track_ids.into_iter().join(","); let mut params = Query::new(); if let Some(market) = market { @@ -235,8 +230,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-artist/) #[maybe_async] - pub async fn artist(&self, artist_id: Id<'_>) -> ClientResult { - let url = format!("artists/{}", artist_id.check_type(Type::Artist)?.id()); + pub async fn artist(&self, artist_id: Id<'_, idtypes::Artist>) -> ClientResult { + let url = format!("artists/{}", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -250,12 +245,9 @@ impl Spotify { #[maybe_async] pub async fn artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = artist_ids - .into_iter() - .map(|id| id.check_type(Type::Artist)) - .try_join(",")?; + let ids = artist_ids.into_iter().join(","); let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -276,7 +268,7 @@ impl Spotify { #[maybe_async] pub async fn artist_albums( &self, - artist_id: Id<'_>, + artist_id: Id<'_, idtypes::Artist>, album_type: Option, country: Option, limit: Option, @@ -295,10 +287,7 @@ impl Spotify { if let Some(country) = country { params.insert("country".to_owned(), country.to_string()); } - let url = format!( - "artists/{}/albums", - artist_id.check_type(Type::Artist)?.id() - ); + let url = format!("artists/{}/albums", artist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -314,7 +303,7 @@ impl Spotify { #[maybe_async] pub async fn artist_top_tracks>>( &self, - artist_id: Id<'_>, + artist_id: Id<'_, idtypes::Artist>, country: T, ) -> ClientResult> { let mut params = Query::with_capacity(1); @@ -323,10 +312,7 @@ impl Spotify { country.into().unwrap_or(Country::UnitedStates).to_string(), ); - let url = format!( - "artists/{}/top-tracks", - artist_id.check_type(Type::Artist)?.id() - ); + let url = format!("artists/{}/top-tracks", artist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result::(&result).map(|x| x.tracks) } @@ -340,11 +326,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-related-artists/) #[maybe_async] - pub async fn artist_related_artists(&self, artist_id: Id<'_>) -> ClientResult> { - let url = format!( - "artists/{}/related-artists", - artist_id.check_type(Type::Artist)?.id() - ); + pub async fn artist_related_artists( + &self, + artist_id: Id<'_, idtypes::Artist>, + ) -> ClientResult> { + let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result::(&result) .map(|x| x.artists) @@ -357,8 +343,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-album/) #[maybe_async] - pub async fn album(&self, album_id: Id<'_>) -> ClientResult { - let url = format!("albums/{}", album_id.check_type(Type::Album)?.id()); + pub async fn album(&self, album_id: Id<'_, idtypes::Album>) -> ClientResult { + let url = format!("albums/{}", album_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -373,12 +359,9 @@ impl Spotify { #[maybe_async] pub async fn albums<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = album_ids - .into_iter() - .map(|id| id.check_type(Type::Album)) - .try_join(",")?; + let ids = album_ids.into_iter().join(","); let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result::(&result).map(|x| x.albums) @@ -436,14 +419,14 @@ impl Spotify { #[maybe_async] pub async fn album_track>, O: Into>>( &self, - album_id: Id<'_>, + album_id: Id<'_, idtypes::Album>, limit: L, offset: O, ) -> ClientResult> { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let url = format!("albums/{}/tracks", album_id.check_type(Type::Album)?.id()); + let url = format!("albums/{}/tracks", album_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -455,8 +438,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-users-profile/) #[maybe_async] - pub async fn user(&self, user_id: Id<'_>) -> ClientResult { - let url = format!("users/{}", user_id.check_type(Type::User)?.id()); + pub async fn user(&self, user_id: Id<'_, idtypes::User>) -> ClientResult { + let url = format!("users/{}", user_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -471,7 +454,7 @@ impl Spotify { #[maybe_async] pub async fn playlist( &self, - playlist_id: Id<'_>, + playlist_id: Id<'_, idtypes::Playlist>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -483,7 +466,7 @@ impl Spotify { params.insert("market".to_owned(), market.to_string()); } - let url = format!("playlists/{}", playlist_id.check_type(Type::Playlist)?.id()); + let url = format!("playlists/{}", playlist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -544,7 +527,7 @@ impl Spotify { pub async fn user_playlist( &self, user_id: &str, - playlist_id: Option>, + playlist_id: Option>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -557,11 +540,7 @@ impl Spotify { } match playlist_id { Some(playlist_id) => { - let url = format!( - "users/{}/playlists/{}", - user_id, - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("users/{}/playlists/{}", user_id, playlist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -586,7 +565,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_tracks>, O: Into>>( &self, - playlist_id: Id<'_>, + playlist_id: Id<'_, idtypes::Playlist>, fields: Option<&str>, limit: L, offset: O, @@ -601,10 +580,7 @@ impl Spotify { if let Some(fields) = fields { params.insert("fields".to_owned(), fields.to_owned()); } - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -697,23 +673,17 @@ impl Spotify { #[maybe_async] pub async fn playlist_add_tracks<'a>( &self, - playlist_id: Id<'_>, - track_ids: impl IntoIterator>, + playlist_id: Id<'_, idtypes::Playlist>, + track_ids: impl IntoIterator>, position: Option, ) -> ClientResult { - let uris = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track).map(|id| id.uri())) - .collect::, _>>()?; + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); let mut params = json!({ "uris": uris }); if let Some(position) = position { json_insert!(params, "position", position); } - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.post(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -729,19 +699,13 @@ impl Spotify { #[maybe_async] pub async fn playlist_replace_tracks<'a>( &self, - playlist_id: Id<'_>, - track_ids: impl IntoIterator>, + playlist_id: Id<'_, idtypes::Playlist>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let uris = track_ids - .into_iter() - .map(|id| id.check_type(Type::Playlist).map(|id| id.uri())) - .collect::, _>>()?; + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); let params = json!({ "uris": uris }); - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); self.put(&url, None, ¶ms).await?; Ok(()) @@ -760,7 +724,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_reorder_tracks>>( &self, - playlist_id: Id<'_>, + playlist_id: Id<'_, idtypes::Playlist>, range_start: i32, range_length: R, insert_before: i32, @@ -775,10 +739,7 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.put(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -794,14 +755,11 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, - playlist_id: Id<'_>, - track_ids: impl IntoIterator>, + playlist_id: Id<'_, idtypes::Playlist>, + track_ids: impl IntoIterator>, snapshot_id: Option, ) -> ClientResult { - let tracks = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track).map(|id| id.uri())) - .collect::, _>>()?; + let tracks = track_ids.into_iter().map(|id| id.uri()).collect::>(); let mut params = json!({ "tracks": tracks.into_iter().map(|uri| { let mut map = Map::new(); @@ -814,10 +772,7 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -854,7 +809,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_specific_occurrences_of_tracks( &self, - playlist_id: Id<'_>, + playlist_id: Id<'_, idtypes::Playlist>, tracks: Vec>, snapshot_id: Option, ) -> ClientResult { @@ -863,7 +818,7 @@ impl Spotify { let mut map = Map::new(); if let Some(_uri) = track.get("uri") { let uri = - Id::from_id_or_uri(Type::Track, &_uri.as_str().unwrap().to_owned())?.uri(); + Id::::from_id_or_uri(&_uri.as_str().unwrap().to_owned())?.uri(); map.insert("uri".to_owned(), uri.into()); } if let Some(_position) = track.get("position") { @@ -876,10 +831,7 @@ impl Spotify { if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!( - "playlists/{}/tracks", - playlist_id.check_type(Type::Playlist)?.id() - ); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.delete(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -1051,12 +1003,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1073,12 +1022,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1093,12 +1039,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1197,12 +1140,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = album_ids - .into_iter() - .map(|id| id.check_type(Type::Album)) - .try_join(",")?; + let ids = album_ids.into_iter().join(","); let url = format!("me/albums/?ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1218,12 +1158,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = album_ids - .into_iter() - .map(|id| id.check_type(Type::Album)) - .try_join(",")?; + let ids = album_ids.into_iter().join(","); let url = format!("me/albums/?ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1240,12 +1177,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = album_ids - .into_iter() - .map(|id| id.check_type(Type::Album)) - .try_join(",")?; + let ids = album_ids.into_iter().join(","); let url = format!("me/albums/contains/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1260,12 +1194,9 @@ impl Spotify { #[maybe_async] pub async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = artist_ids - .into_iter() - .map(|id| id.check_type(Type::Artist)) - .try_join(",")?; + let ids = artist_ids.into_iter().join(","); let url = format!("me/following?type=artist&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1281,12 +1212,9 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = artist_ids - .into_iter() - .map(|id| id.check_type(Type::Artist)) - .try_join(",")?; + let ids = artist_ids.into_iter().join(","); let url = format!("me/following?type=artist&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1303,12 +1231,9 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = artist_ids - .into_iter() - .map(|id| id.check_type(Type::Artist)) - .try_join(",")?; + let ids = artist_ids.into_iter().join(","); let url = format!("me/following/contains?type=artist&ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1323,12 +1248,9 @@ impl Spotify { #[maybe_async] pub async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = user_ids - .into_iter() - .map(|id| id.check_type(Type::User)) - .try_join(",")?; + let ids = user_ids.into_iter().join(","); let url = format!("me/following?type=user&ids={}", ids); self.put(&url, None, &json!({})).await?; @@ -1344,12 +1266,9 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = user_ids - .into_iter() - .map(|id| id.check_type(Type::User)) - .try_join(",")?; + let ids = user_ids.into_iter().join(","); let url = format!("me/following?type=user&ids={}", ids); self.delete(&url, None, &json!({})).await?; @@ -1511,9 +1430,9 @@ impl Spotify { #[maybe_async] pub async fn recommendations>>( &self, - seed_artists: Option>>, + seed_artists: Option>>, seed_genres: Option>, - seed_tracks: Option>>, + seed_tracks: Option>>, limit: L, country: Option, payload: &Map, @@ -1550,10 +1469,7 @@ impl Spotify { } if let Some(seed_artists) = seed_artists { - let seed_artists_ids = seed_artists - .into_iter() - .map(|id| id.check_type(Type::Artist)) - .try_join(",")?; + let seed_artists_ids = seed_artists.into_iter().join(","); params.insert("seed_artists".to_owned(), seed_artists_ids); } @@ -1562,10 +1478,7 @@ impl Spotify { } if let Some(seed_tracks) = seed_tracks { - let seed_tracks_ids = seed_tracks - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let seed_tracks_ids = seed_tracks.into_iter().join(","); params.insert("seed_tracks".to_owned(), seed_tracks_ids); } @@ -1584,8 +1497,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-features/) #[maybe_async] - pub async fn track_features(&self, track_id: Id<'_>) -> ClientResult { - let url = format!("audio-features/{}", track_id.check_type(Type::Track)?.id()); + pub async fn track_features( + &self, + track_id: Id<'_, idtypes::Track>, + ) -> ClientResult { + let url = format!("audio-features/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1599,12 +1515,9 @@ impl Spotify { #[maybe_async] pub async fn tracks_features<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult>> { - let ids = track_ids - .into_iter() - .map(|id| id.check_type(Type::Track)) - .try_join(",")?; + let ids = track_ids.into_iter().join(","); let url = format!("audio-features/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -1623,8 +1536,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-analysis/) #[maybe_async] - pub async fn track_analysis(&self, track_id: Id<'_>) -> ClientResult { - let url = format!("audio-analysis/{}", track_id.check_type(Type::Track)?.id()); + pub async fn track_analysis( + &self, + track_id: Id<'_, idtypes::Track>, + ) -> ClientResult { + let url = format!("audio-analysis/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1981,12 +1897,16 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/shows/get-a-show/) #[maybe_async] - pub async fn get_a_show(&self, id: Id<'_>, market: Option) -> ClientResult { + pub async fn get_a_show( + &self, + id: Id<'_, idtypes::Show>, + market: Option, + ) -> ClientResult { let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!("shows/{}", id.check_type(Type::Show)?.id()); + let url = format!("shows/{}", id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2034,7 +1954,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: Id<'_>, + id: Id<'_, idtypes::Show>, limit: L, offset: O, market: Option, @@ -2045,7 +1965,7 @@ impl Spotify { if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } - let url = format!("shows/{}/episodes", id.check_type(Type::Show)?.id()); + let url = format!("shows/{}/episodes", id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -2062,10 +1982,10 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: Id<'_>, + id: Id<'_, idtypes::Episode>, market: Option, ) -> ClientResult { - let url = format!("episodes/{}", id.check_type(Type::Episode)?.id()); + let url = format!("episodes/{}", id.id()); let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); @@ -2159,13 +2079,9 @@ mod tests { } #[test] - fn test_try_join() { - let data: Vec> = vec![Ok("a"), Ok("b"), Ok("c")]; - let joined: Result<_, u32> = data.into_iter().try_join(","); - assert_eq!("a,b,c", joined.unwrap()); - - let data: Vec> = vec![Ok("a"), Err(1), Ok("c")]; - let joined: Result<_, u32> = data.into_iter().try_join(","); - assert_eq!(1, joined.unwrap_err()); + fn test_join() { + let data = vec!["a", "b", "c"]; + let joined = data.into_iter().join(","); + assert_eq!("a,b,c", &joined); } } diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index da5773e6..3c5831ef 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -41,6 +41,51 @@ pub enum Type { Episode, } +pub mod idtypes { + use super::Type; + + pub trait IdType: PartialEq { + const TYPE: Type; + } + + impl IdType for Artist { + const TYPE: Type = Type::Artist; + } + impl IdType for Album { + const TYPE: Type = Type::Album; + } + impl IdType for Track { + const TYPE: Type = Type::Track; + } + impl IdType for Playlist { + const TYPE: Type = Type::Playlist; + } + impl IdType for User { + const TYPE: Type = Type::User; + } + impl IdType for Show { + const TYPE: Type = Type::Show; + } + impl IdType for Episode { + const TYPE: Type = Type::Episode; + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Artist {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Album {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Track {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Playlist {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum User {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Show {} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Episode {} +} + /// Additional typs: `track`, `episode` /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/get-information-about-the-users-current-playback/) diff --git a/src/model/mod.rs b/src/model/mod.rs index 56b8eee0..a3ce9242 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -171,12 +171,50 @@ pub enum PlayingItem { } /// A Spotify object id of given [type](crate::model::enums::types::Type) -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Id<'id> { - _type: Type, +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Id<'id, T> { + _type: PhantomData, id: &'id str, } +impl<'id, T> Id<'id, T> { + pub fn to_owned(&self) -> IdBuf { + IdBuf { + _type: PhantomData, + id: self.id.to_owned(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct IdBuf { + _type: PhantomData, + id: String, +} + +impl<'id, T> Into> for &'id IdBuf { + fn into(self) -> Id<'id, T> { + Id { + _type: PhantomData, + id: self.id(), + } + } +} + +impl IdBuf { + pub fn as_ref(&self) -> Id<'_, T> { + self.into() + } + + pub fn _type(&self) -> Type { + T::TYPE + } + + pub fn id(&self) -> &str { + &self.id + } +} + /// Spotify id or URI parsing error /// /// See also [`Id`](crate::model::Id) for details. @@ -193,49 +231,41 @@ pub enum IdError { InvalidId, } -impl std::fmt::Display for Id<'_> { +impl std::fmt::Display for Id<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "spotify:{}:{}", self._type, self.id) + write!(f, "spotify:{}:{}", T::TYPE, self.id) } } -impl AsRef for Id<'_> { +impl AsRef for Id<'_, T> { fn as_ref(&self) -> &str { - self.id + &self.id } } -impl Id<'_> { +impl Id<'_, T> { /// Spotify object type pub fn _type(&self) -> Type { - self._type - } - - pub(in crate) fn check_type(self, _type: Type) -> Result { - if self._type == _type { - Ok(self) - } else { - Err(IdError::InvalidType) - } + T::TYPE } /// Spotify object id (guaranteed to be a string of alphanumeric characters) pub fn id(&self) -> &str { - &self.id + self.id } /// Spotify object URI in a well-known format: spotify:type:id /// /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. pub fn uri(&self) -> String { - format!("spotify:{}:{}", self._type, self.id) + format!("spotify:{}:{}", T::TYPE, &self.id) } /// Full Spotify object URL, can be opened in a browser /// /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI pub fn url(&self) -> String { - format!("https://open.spotify.com/{}/{}", self._type, self.id) + format!("https://open.spotify.com/{}/{}", T::TYPE, &self.id) } /// Parse Spotify id or URI from string slice @@ -256,11 +286,10 @@ impl Id<'_> { /// - `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 non-alphanumeric characters), /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. - pub fn from_id_or_uri<'a, 'b: 'a>(_type: Type, id_or_uri: &'b str) -> Result, IdError> { - match Self::from_uri(id_or_uri) { - Ok(id) if id._type == _type => Ok(id), - Ok(_) => Err(IdError::InvalidType), - Err(IdError::InvalidPrefix) => Self::from_id(_type, id_or_uri), + pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result, IdError> { + match Id::::from_uri(id_or_uri) { + Ok(id) => Ok(id), + Err(IdError::InvalidPrefix) => Id::::from_id(id_or_uri), Err(error) => Err(error), } } @@ -272,9 +301,12 @@ impl Id<'_> { /// # Errors: /// /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. - pub fn from_id<'a, 'b: 'a>(_type: Type, id: &'b str) -> Result, IdError> { + pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result, IdError> { if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { - Ok(Id { _type, id }) + Ok(Id { + _type: PhantomData, + id, + }) } else { Err(IdError::InvalidId) } @@ -294,7 +326,7 @@ impl Id<'_> { /// - `IdError::InvalidType` - if type part of an `uri` is not a valid Spotify type, /// - `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. - pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { + pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; let sep = match rest.chars().next() { Some(ch) if ch == '/' || ch == ':' => ch, @@ -303,14 +335,19 @@ impl Id<'_> { let rest = &rest[1..]; if let Some((tpe, id)) = rest.rfind(sep).map(|mid| rest.split_at(mid)) { - let _type = tpe.parse().map_err(|_| IdError::InvalidType)?; - Self::from_id(_type, &id[1..]) + let _type: Type = tpe.parse().map_err(|_| IdError::InvalidType)?; + if _type != T::TYPE { + return Err(IdError::InvalidType); + } + Id::::from_id(&id[1..]) } else { Err(IdError::InvalidFormat) } } } +use crate::model::enums::types::idtypes::IdType; +use std::marker::PhantomData; pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, @@ -325,33 +362,35 @@ mod tests { fn test_get_id() { // Assert artist let artist_id = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; - let id = Id::from_id_or_uri(Type::Artist, artist_id).unwrap(); + let id = Id::::from_id_or_uri(artist_id).unwrap(); assert_eq!("2WX2uTcsvV5OnS0inACecP", id.id()); // Assert album let album_id_a = "spotify/album/2WX2uTcsvV5OnS0inACecP"; assert_eq!( "2WX2uTcsvV5OnS0inACecP", - Id::from_id_or_uri(Type::Album, album_id_a).unwrap().id() + Id::::from_id_or_uri(album_id_a) + .unwrap() + .id() ); // Mismatch type assert_eq!( Err(IdError::InvalidType), - Id::from_id_or_uri(Type::Artist, album_id_a) + Id::::from_id_or_uri(album_id_a) ); // Could not split let artist_id_c = "spotify-album-2WX2uTcsvV5OnS0inACecP"; assert_eq!( Err(IdError::InvalidId), - Id::from_id_or_uri(Type::Artist, artist_id_c) + Id::::from_id_or_uri(artist_id_c) ); let playlist_id = "spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"; assert_eq!( "59ZbFPES4DQwEjBpWHzrtC", - Id::from_id_or_uri(Type::Playlist, playlist_id) + Id::::from_id_or_uri(playlist_id) .unwrap() .id() ); @@ -361,8 +400,8 @@ mod tests { fn test_get_uri() { let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "1301WleyT98MSxVHPZCA6M"; - let id1 = Id::from_id_or_uri(Type::Track, track_id1).unwrap(); - let id2 = Id::from_id_or_uri(Type::Track, track_id2).unwrap(); + let id1 = Id::::from_id_or_uri(track_id1).unwrap(); + let id2 = Id::::from_id_or_uri(track_id2).unwrap(); assert_eq!(track_id1, &id1.uri()); assert_eq!("spotify:track:1301WleyT98MSxVHPZCA6M", &id2.uri()); } diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 47f42097..6c4939c9 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -2,7 +2,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; -use rspotify::model::{AlbumType, Country, Id, Type}; +use rspotify::model::{AlbumType, Country, Id}; use rspotify::oauth2::CredentialsBuilder; use maybe_async::maybe_async; @@ -110,7 +110,7 @@ async fn test_artist_top_tracks() { #[maybe_async] #[maybe_async_test] async fn test_audio_analysis() { - let track = Id::from_id(Type::Track, "06AKEBrKUckW0KREUWRnvT").unwrap(); + let track = Id::from_id("06AKEBrKUckW0KREUWRnvT").unwrap(); creds_client().await.track_analysis(track).await.unwrap(); } @@ -139,7 +139,7 @@ async fn test_audios_features() { #[maybe_async] #[maybe_async_test] async fn test_user() { - let birdy_uri = Id::from_id(Type::User, "tuggareutangranser").unwrap(); + let birdy_uri = Id::from_id("tuggareutangranser").unwrap(); creds_client().await.user(birdy_uri).await.unwrap(); } @@ -164,11 +164,7 @@ async fn test_tracks() { async fn test_existing_playlist() { creds_client() .await - .playlist( - Id::from_id(Type::Playlist, "37i9dQZF1DZ06evO45P0Eo").unwrap(), - None, - None, - ) + .playlist(Id::from_id("37i9dQZF1DZ06evO45P0Eo").unwrap(), None, None) .await .unwrap(); } @@ -178,7 +174,7 @@ async fn test_existing_playlist() { async fn test_fake_playlist() { let playlist = creds_client() .await - .playlist(Id::from_id(Type::Playlist, "fakeid").unwrap(), None, None) + .playlist(Id::from_id("fakeid").unwrap(), None, None) .await; assert!(!playlist.is_ok()); } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 25953b08..0ceb8810 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -19,7 +19,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; -use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange, Type}; +use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use std::env; @@ -170,8 +170,8 @@ async fn test_current_user_saved_albums_add() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(Id::from_id(Type::Album, album_id2).unwrap()); - album_ids.push(Id::from_id(Type::Album, album_id1).unwrap()); + album_ids.push(Id::from_id(album_id2).unwrap()); + album_ids.push(Id::from_id(album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_add(album_ids) @@ -186,8 +186,8 @@ async fn test_current_user_saved_albums_delete() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(Id::from_id(Type::Album, album_id2).unwrap()); - album_ids.push(Id::from_id(Type::Album, album_id1).unwrap()); + album_ids.push(Id::from_id(album_id2).unwrap()); + album_ids.push(Id::from_id(album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_delete(album_ids) @@ -365,8 +365,8 @@ async fn test_previous_playback() { #[ignore] async fn test_recommendations() { let mut payload = Map::new(); - let seed_artists = vec![Id::from_id(Type::Artist, "4NHQUGzhtTLFvgF5SZesLK").unwrap()]; - let seed_tracks = vec![Id::from_id(Type::Track, "0c6xIDDpzE81m2q797ordA").unwrap()]; + let seed_artists = vec![Id::from_id("4NHQUGzhtTLFvgF5SZesLK").unwrap()]; + let seed_tracks = vec![Id::from_id("0c6xIDDpzE81m2q797ordA").unwrap()]; payload.insert("min_energy".to_owned(), 0.4.into()); payload.insert("min_popularity".to_owned(), 50.into()); oauth_client() @@ -509,8 +509,8 @@ async fn test_user_follow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(Id::from_id(Type::Artist, artist_id2).unwrap()); - artists.push(Id::from_id(Type::Artist, artist_id1).unwrap()); + artists.push(Id::from_id(artist_id2).unwrap()); + artists.push(Id::from_id(artist_id1).unwrap()); oauth_client() .await .user_follow_artists(artists) @@ -525,8 +525,8 @@ async fn test_user_unfollow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(Id::from_id(Type::Artist, artist_id2).unwrap()); - artists.push(Id::from_id(Type::Artist, artist_id1).unwrap()); + artists.push(Id::from_id(artist_id2).unwrap()); + artists.push(Id::from_id(artist_id1).unwrap()); oauth_client() .await .user_unfollow_artists(artists) @@ -539,7 +539,7 @@ async fn test_user_unfollow_artist() { #[ignore] async fn test_user_follow_users() { let mut users = vec![]; - let user_id1 = Id::from_id(Type::User, "exampleuser01").unwrap(); + let user_id1 = Id::from_id("exampleuser01").unwrap(); users.push(user_id1); oauth_client().await.user_follow_users(users).await.unwrap(); } @@ -549,7 +549,7 @@ async fn test_user_follow_users() { #[ignore] async fn test_user_unfollow_users() { let mut users = vec![]; - let user_id1 = Id::from_id(Type::User, "exampleuser01").unwrap(); + let user_id1 = Id::from_id("exampleuser01").unwrap(); users.push(user_id1); oauth_client() .await @@ -562,7 +562,7 @@ async fn test_user_unfollow_users() { #[maybe_async_test] #[ignore] async fn test_playlist_add_tracks() { - let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); tracks_ids.push(track_id1); @@ -634,7 +634,7 @@ async fn test_playlist_follow_playlist() { #[maybe_async_test] #[ignore] async fn test_playlist_recorder_tracks() { - let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let range_start = 0; let insert_before = 1; let range_length = 1; @@ -649,7 +649,7 @@ async fn test_playlist_recorder_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_all_occurrences_of_tracks() { - let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); @@ -666,7 +666,7 @@ async fn test_playlist_remove_all_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_specific_occurrences_of_tracks() { - let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks = vec![]; let mut map1 = Map::new(); let mut position1 = vec![]; @@ -698,7 +698,7 @@ async fn test_playlist_remove_specific_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_replace_tracks() { - let playlist_id = Id::from_id(Type::Playlist, "5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); @@ -716,7 +716,7 @@ async fn test_playlist_replace_tracks() { #[ignore] async fn test_user_playlist() { let user_id = "spotify"; - let playlist_id = Id::from_id(Type::Playlist, "59ZbFPES4DQwEjBpWHzrtC").unwrap(); + let playlist_id = Id::from_id("59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await .user_playlist(user_id, Some(playlist_id), None, None) From ae250f3a1f7f77a0e8929beebe22bd94ffce6d15 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 15:16:59 +0300 Subject: [PATCH 21/59] update docs, FromStr for IdBuf --- src/model/enums/idtypes.rs | 53 ++++++++++++++++++++++++++++++++++++++ src/model/enums/mod.rs | 1 + src/model/enums/types.rs | 45 -------------------------------- src/model/mod.rs | 30 ++++++++++++++++++--- 4 files changed, 80 insertions(+), 49 deletions(-) create mode 100644 src/model/enums/idtypes.rs diff --git a/src/model/enums/idtypes.rs b/src/model/enums/idtypes.rs new file mode 100644 index 00000000..0b902204 --- /dev/null +++ b/src/model/enums/idtypes.rs @@ -0,0 +1,53 @@ +use crate::model::Type; + +mod private { + pub trait Sealed {} +} + +pub trait IdType: private::Sealed { + const TYPE: Type; +} + +impl IdType for Artist { + const TYPE: Type = Type::Artist; +} +impl IdType for Album { + const TYPE: Type = Type::Album; +} +impl IdType for Track { + const TYPE: Type = Type::Track; +} +impl IdType for Playlist { + const TYPE: Type = Type::Playlist; +} +impl IdType for User { + const TYPE: Type = Type::User; +} +impl IdType for Show { + const TYPE: Type = Type::Show; +} +impl IdType for Episode { + const TYPE: Type = Type::Episode; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Artist {} +impl private::Sealed for Artist {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Album {} +impl private::Sealed for Album {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Track {} +impl private::Sealed for Track {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Playlist {} +impl private::Sealed for Playlist {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum User {} +impl private::Sealed for User {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Show {} +impl private::Sealed for Show {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Episode {} +impl private::Sealed for Episode {} diff --git a/src/model/enums/mod.rs b/src/model/enums/mod.rs index 5ee6540a..270160ce 100644 --- a/src/model/enums/mod.rs +++ b/src/model/enums/mod.rs @@ -1,5 +1,6 @@ //! All Enums for Rspotify pub mod country; +pub mod idtypes; pub mod misc; pub mod types; diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index 3c5831ef..da5773e6 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -41,51 +41,6 @@ pub enum Type { Episode, } -pub mod idtypes { - use super::Type; - - pub trait IdType: PartialEq { - const TYPE: Type; - } - - impl IdType for Artist { - const TYPE: Type = Type::Artist; - } - impl IdType for Album { - const TYPE: Type = Type::Album; - } - impl IdType for Track { - const TYPE: Type = Type::Track; - } - impl IdType for Playlist { - const TYPE: Type = Type::Playlist; - } - impl IdType for User { - const TYPE: Type = Type::User; - } - impl IdType for Show { - const TYPE: Type = Type::Show; - } - impl IdType for Episode { - const TYPE: Type = Type::Episode; - } - - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Artist {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Album {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Track {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Playlist {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum User {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Show {} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Episode {} -} - /// Additional typs: `track`, `episode` /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/get-information-about-the-users-current-playback/) diff --git a/src/model/mod.rs b/src/model/mod.rs index a3ce9242..07d68252 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -23,6 +23,8 @@ use std::{fmt, time::Duration}; use strum::Display; use thiserror::Error; +use self::enums::idtypes::IdType; + /// Vistor to help deserialize duration represented as millisecond to `std::time::Duration` struct DurationVisitor; impl<'de> de::Visitor<'de> for DurationVisitor { @@ -171,6 +173,9 @@ pub enum PlayingItem { } /// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is a not-owning type, it stores a &str only. +/// See [IdBuf](crate::model::IdBuf) for owned version of the type. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Id<'id, T> { _type: PhantomData, @@ -186,6 +191,13 @@ impl<'id, T> Id<'id, T> { } } +/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is an owning type, it stores a String. +/// See [IdBuf](crate::model::Id) for light-weight non-owning type. +/// +/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned` or `Id::from_id_or_uri(val).to_owned()` +/// to construct an instance of this type. #[derive(Debug, PartialEq, Eq, Clone)] pub struct IdBuf { _type: PhantomData, @@ -196,20 +208,23 @@ impl<'id, T> Into> for &'id IdBuf { fn into(self) -> Id<'id, T> { Id { _type: PhantomData, - id: self.id(), + id: &self.id, } } } impl IdBuf { + /// Get a non-owning [`Id`] representation of the id pub fn as_ref(&self) -> Id<'_, T> { self.into() } + /// Get a [`Type`](crate::model::enums::types::Type) of the id pub fn _type(&self) -> Type { T::TYPE } + /// Get id value as a &str pub fn id(&self) -> &str { &self.id } @@ -243,6 +258,14 @@ impl AsRef for Id<'_, T> { } } +impl std::str::FromStr for IdBuf { + type Err = IdError; + + fn from_str(s: &str) -> Result { + Id::from_id_or_uri(s).map(|id| id.to_owned()) + } +} + impl Id<'_, T> { /// Spotify object type pub fn _type(&self) -> Type { @@ -273,7 +296,7 @@ impl Id<'_, T> { /// 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 alphanumeric string. - /// The URI must be of given `_type`, otherwise `IdError::InvalidType` error is returned. + /// The URI must be of given `T`ype, otherwise `IdError::InvalidType` error is returned. /// /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify/track/4y4VO05kYgUTo2bzbox1an`. /// @@ -323,7 +346,7 @@ impl Id<'_, T> { /// # 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, + /// - `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. pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { @@ -346,7 +369,6 @@ impl Id<'_, T> { } } -use crate::model::enums::types::idtypes::IdType; use std::marker::PhantomData; pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, From 9ed65667564eade7f7f4831d51567251a484f1ce Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 15:26:51 +0300 Subject: [PATCH 22/59] fix intermediate collect --- src/client.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 302b3175..60e6e6a5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -759,14 +759,16 @@ impl Spotify { track_ids: impl IntoIterator>, snapshot_id: Option, ) -> ClientResult { - let tracks = track_ids.into_iter().map(|id| id.uri()).collect::>(); - - let mut params = json!({ "tracks": tracks.into_iter().map(|uri| { - let mut map = Map::new(); - map.insert("uri".to_owned(), uri.into()); - map - }).collect::>() - }); + let tracks = track_ids + .into_iter() + .map(|id| { + let mut map = Map::with_capacity(1); + map.insert("uri".to_owned(), id.uri().into()); + map + }) + .collect::>(); + + let mut params = json!({ "tracks": tracks }); if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); From a5d62426951f3fb71bcc43d7722113f4e93816d6 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 15:47:42 +0300 Subject: [PATCH 23/59] add TrackIdOrPos type, update docs --- CHANGELOG.md | 12 +++++++----- src/client.rs | 14 ++++++-------- src/model/track.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb96b21..f7b66af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,15 +106,15 @@ If we missed any change or there's something you'd like to discuss about this ve + The `ClientError::CLI` variant, for whenever user interaction goes wrong - Fix typo in `user_playlist_remove_specific_occurrenes_of_tracks`, now it's `user_playlist_remove_specific_occurrences_of_tracks`. - ([#123](https://github.com/ramsayleung/rspotify/pull/123))All fallible calls in the client return a `ClientError` rather than using `failure`. -- ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Endpoints take `Vec/&[String]` as parameter have changed to `impl IntoIterator`, which is backward compatibility. - + The endpoints which changes parameter from `Vec` to `impl IntoIterator`: +- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints take `Vec>/&[Id<'_, Type>]` as parameter have changed to `impl IntoIterator>`. + + The endpoints which changes parameter from `Vec` to `impl IntoIterator>`: - `artists` - `albums` - `save_shows` - `get_several_episodes` - `check_users_saved_shows` - `remove_users_saved_shows` - + The endpoints which changes parameter from `&[String]` to `impl IntoIterator`: + + The endpoints which changes parameter from `&[String]` to `impl IntoIterator>`: - `user_playlist_add_tracks` - `user_playlist_replace_tracks` - `user_playlist_remove_all_occurrences_of_tracks` @@ -130,10 +130,12 @@ If we missed any change or there's something you'd like to discuss about this ve - `user_follow_users` - `user_unfollow_users` - `audios_features` - + The endpoints which changes parameter from `String` to `&str`: + + The endpoints which changes parameter from `String` to `Id<'_, Type>`: - `get_a_show` - `get_an_episode` - `get_shows_episodes` + + The endpoint which changes parameter from `Vec>` to `Vec`: + - `playlist_remove_specific_occurrences_of_tracks` - ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Rename endpoints with more fitting name: + `audio_analysis` -> `track_analysis` + `audio_features` -> `track_features` @@ -162,7 +164,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Change `{FullArtist, FullPlaylist, PublicUser, PrivateUser}::followers` from `HashMap>` to struct `Followers` + Replace `Actions::disallows` with a `Vec` by removing all entires whose value is false, which will result in a simpler API + Replace `{FullAlbum, SimplifiedEpisode, FullEpisode}::release_date_precision` from `String` to `DatePrecision` enum, makes it easier to use. - + Added more strict validation for URI and id parameters everywhere, new error variant `ClientError::InvalidId(IdError)` for invalid input ids and URIs. + + Id and URI parameters are type-safe now everywhere, `Id<'_, Type>` and `IdBuf` types for ids/URIs added (non-owning and owning structs). - ([#157](https://github.com/ramsayleung/rspotify/pull/157))Keep polishing models to make it easier to use: + Constrain visibility of `FullArtists` struct with `pub (in crate)`, make `artists` and `artist_related_artists` endpoints return a `Vec` instead. + Constrain visibility of `FullTracks` struct with `pub (in crate)`, make `tracks` and `artist_top_tracks` endpoints return a `Vec` instead. diff --git a/src/client.rs b/src/client.rs index 60e6e6a5..2d17e49f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -812,19 +812,17 @@ impl Spotify { pub async fn playlist_remove_specific_occurrences_of_tracks( &self, playlist_id: Id<'_, idtypes::Playlist>, - tracks: Vec>, + tracks: Vec, snapshot_id: Option, ) -> ClientResult { - let mut ftracks: Vec> = vec![]; + let mut ftracks: Vec> = Vec::with_capacity(tracks.len()); for track in tracks { let mut map = Map::new(); - if let Some(_uri) = track.get("uri") { - let uri = - Id::::from_id_or_uri(&_uri.as_str().unwrap().to_owned())?.uri(); - map.insert("uri".to_owned(), uri.into()); + if let Some(id) = track.id { + map.insert("uri".to_owned(), id.uri().into()); } - if let Some(_position) = track.get("position") { - map.insert("position".to_owned(), _position.to_owned()); + if let Some(pos) = track.pos { + map.insert("position".to_owned(), pos.into()); } ftracks.push(map); } diff --git a/src/model/track.rs b/src/model/track.rs index d8f52162..f7e87762 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -7,8 +7,8 @@ use std::{collections::HashMap, time::Duration}; use super::album::SimplifiedAlbum; use super::artist::SimplifiedArtist; use super::Restriction; -use crate::model::Type; use crate::model::{from_duration_ms, to_duration_ms}; +use crate::model::{idtypes, Id, Type}; /// Full track object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-full) @@ -109,3 +109,29 @@ pub struct SavedTrack { pub added_at: DateTime, pub track: FullTrack, } + +/// Track id or position to point to a concrete track in a playlist +pub struct TrackIdOrPos<'id> { + pub id: Option>, + pub pos: Option, +} + +impl TrackIdOrPos<'static> { + /// Track in a playlist by a position + fn from_pos(pos: u32) -> Self { + TrackIdOrPos { + id: None, + pos: Some(pos), + } + } +} + +impl<'id> TrackIdOrPos<'id> { + /// Track in a playlist by an id + fn from_id(id: Id<'id, idtypes::Track>) -> Self { + TrackIdOrPos { + id: Some(id), + pos: None, + } + } +} From ce556807848a5cad16224d13e21ced6b32347718 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 15:59:52 +0300 Subject: [PATCH 24/59] fix playlist_remove_specific_occurences_of_track method --- CHANGELOG.md | 2 +- src/client.rs | 10 +++------- src/model/track.rs | 27 +++++++-------------------- tests/test_with_oauth.rs | 32 +++++++++++--------------------- 4 files changed, 22 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b66af2..08570fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,7 +134,7 @@ If we missed any change or there's something you'd like to discuss about this ve - `get_a_show` - `get_an_episode` - `get_shows_episodes` - + The endpoint which changes parameter from `Vec>` to `Vec`: + + The endpoint which changes parameter from `Vec>` to `Vec`: - `playlist_remove_specific_occurrences_of_tracks` - ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Rename endpoints with more fitting name: + `audio_analysis` -> `track_analysis` diff --git a/src/client.rs b/src/client.rs index 2d17e49f..3d34309b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -812,18 +812,14 @@ impl Spotify { pub async fn playlist_remove_specific_occurrences_of_tracks( &self, playlist_id: Id<'_, idtypes::Playlist>, - tracks: Vec, + tracks: Vec>, snapshot_id: Option, ) -> ClientResult { let mut ftracks: Vec> = Vec::with_capacity(tracks.len()); for track in tracks { let mut map = Map::new(); - if let Some(id) = track.id { - map.insert("uri".to_owned(), id.uri().into()); - } - if let Some(pos) = track.pos { - map.insert("position".to_owned(), pos.into()); - } + map.insert("uri".to_owned(), track.id.uri().into()); + map.insert("positions".to_owned(), track.positions.into()); ftracks.push(map); } diff --git a/src/model/track.rs b/src/model/track.rs index f7e87762..42f50850 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -110,28 +110,15 @@ pub struct SavedTrack { pub track: FullTrack, } -/// Track id or position to point to a concrete track in a playlist -pub struct TrackIdOrPos<'id> { - pub id: Option>, - pub pos: Option, +/// Track id with specific positions track in a playlist +pub struct TrackPositions<'id> { + pub id: Id<'id, idtypes::Track>, + pub positions: Vec, } -impl TrackIdOrPos<'static> { - /// Track in a playlist by a position - fn from_pos(pos: u32) -> Self { - TrackIdOrPos { - id: None, - pos: Some(pos), - } - } -} - -impl<'id> TrackIdOrPos<'id> { +impl<'id> TrackPositions<'id> { /// Track in a playlist by an id - fn from_id(id: Id<'id, idtypes::Track>) -> Self { - TrackIdOrPos { - id: Some(id), - pos: None, - } + pub fn new(id: Id<'id, idtypes::Track>, positions: Vec) -> Self { + Self { id, positions } } } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 0ceb8810..2da2270d 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -19,7 +19,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; -use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange}; +use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange, TrackPositions}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use std::env; @@ -667,26 +667,16 @@ async fn test_playlist_remove_all_occurrences_of_tracks() { #[ignore] async fn test_playlist_remove_specific_occurrences_of_tracks() { let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); - let mut tracks = vec![]; - let mut map1 = Map::new(); - let mut position1 = vec![]; - position1.push(0); - position1.push(3); - map1.insert( - "uri".to_string(), - "spotify:track:4iV5W9uYEdYUVa79Axb7Rh".into(), - ); - map1.insert("position".to_string(), position1.into()); - tracks.push(map1); - let mut map2 = Map::new(); - let mut position2 = vec![]; - position2.push(7); - map2.insert( - "uri".to_string(), - "spotify:track:1301WleyT98MSxVHPZCA6M".into(), - ); - map2.insert("position".to_string(), position2.into()); - tracks.push(map2); + let tracks = vec![ + TrackPositions::new( + Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), + vec![0, 3], + ), + TrackPositions::new( + Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(), + vec![7], + ), + ]; oauth_client() .await .playlist_remove_specific_occurrences_of_tracks(playlist_id, tracks, None) From c574e0695d7e728e134d9139163f868d4737b538 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 16:06:12 +0300 Subject: [PATCH 25/59] remove ClientError::IdError error variant: everything's type-safe now --- src/client.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3d34309b..1a495b90 100644 --- a/src/client.rs +++ b/src/client.rs @@ -81,9 +81,6 @@ pub enum ClientError { #[error("cache file error: {0}")] CacheFile(String), - - #[error("id parse error: {0:?}")] - InvalidId(#[from] IdError), } pub type ClientResult = Result; From c22c9a6edffc348637a164854ce9f574bbcff204 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 24 Dec 2020 17:45:26 +0300 Subject: [PATCH 26/59] use iterator to build query --- src/client.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1a495b90..3cf17488 100644 --- a/src/client.rs +++ b/src/client.rs @@ -812,13 +812,15 @@ impl Spotify { tracks: Vec>, snapshot_id: Option, ) -> ClientResult { - let mut ftracks: Vec> = Vec::with_capacity(tracks.len()); - for track in tracks { - let mut map = Map::new(); - map.insert("uri".to_owned(), track.id.uri().into()); - map.insert("positions".to_owned(), track.positions.into()); - ftracks.push(map); - } + let ftracks = tracks + .into_iter() + .map(|track| { + let mut map = Map::new(); + map.insert("uri".to_owned(), track.id.uri().into()); + map.insert("positions".to_owned(), track.positions.into()); + map + }) + .collect::>(); let mut params = json!({ "tracks": ftracks }); if let Some(snapshot_id) = snapshot_id { From f055e96fc72317b2477e453d126928931fe8c5f6 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 00:13:11 +0300 Subject: [PATCH 27/59] move id types into a idtypes module --- src/model/enums/idtypes.rs | 53 -------- src/model/enums/mod.rs | 1 - src/model/idtypes.rs | 253 +++++++++++++++++++++++++++++++++++++ src/model/mod.rs | 223 +++----------------------------- src/model/track.rs | 1 + 5 files changed, 273 insertions(+), 258 deletions(-) delete mode 100644 src/model/enums/idtypes.rs create mode 100644 src/model/idtypes.rs diff --git a/src/model/enums/idtypes.rs b/src/model/enums/idtypes.rs deleted file mode 100644 index 0b902204..00000000 --- a/src/model/enums/idtypes.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::model::Type; - -mod private { - pub trait Sealed {} -} - -pub trait IdType: private::Sealed { - const TYPE: Type; -} - -impl IdType for Artist { - const TYPE: Type = Type::Artist; -} -impl IdType for Album { - const TYPE: Type = Type::Album; -} -impl IdType for Track { - const TYPE: Type = Type::Track; -} -impl IdType for Playlist { - const TYPE: Type = Type::Playlist; -} -impl IdType for User { - const TYPE: Type = Type::User; -} -impl IdType for Show { - const TYPE: Type = Type::Show; -} -impl IdType for Episode { - const TYPE: Type = Type::Episode; -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Artist {} -impl private::Sealed for Artist {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Album {} -impl private::Sealed for Album {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Track {} -impl private::Sealed for Track {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Playlist {} -impl private::Sealed for Playlist {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum User {} -impl private::Sealed for User {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Show {} -impl private::Sealed for Show {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Episode {} -impl private::Sealed for Episode {} diff --git a/src/model/enums/mod.rs b/src/model/enums/mod.rs index 270160ce..5ee6540a 100644 --- a/src/model/enums/mod.rs +++ b/src/model/enums/mod.rs @@ -1,6 +1,5 @@ //! All Enums for Rspotify pub mod country; -pub mod idtypes; pub mod misc; pub mod types; diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs new file mode 100644 index 00000000..32e95275 --- /dev/null +++ b/src/model/idtypes.rs @@ -0,0 +1,253 @@ +use crate::model::Type; +use serde::export::PhantomData; +use strum::Display; +use thiserror::Error; + +mod private { + pub trait Sealed {} +} + +pub trait IdType: private::Sealed { + const TYPE: Type; +} + +impl IdType for Artist { + const TYPE: Type = Type::Artist; +} +impl IdType for Album { + const TYPE: Type = Type::Album; +} +impl IdType for Track { + const TYPE: Type = Type::Track; +} +impl IdType for Playlist { + const TYPE: Type = Type::Playlist; +} +impl IdType for User { + const TYPE: Type = Type::User; +} +impl IdType for Show { + const TYPE: Type = Type::Show; +} +impl IdType for Episode { + const TYPE: Type = Type::Episode; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Artist {} +impl private::Sealed for Artist {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Album {} +impl private::Sealed for Album {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Track {} +impl private::Sealed for Track {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Playlist {} +impl private::Sealed for Playlist {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum User {} +impl private::Sealed for User {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Show {} +impl private::Sealed for Show {} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Episode {} +impl private::Sealed for Episode {} + +/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is a not-owning type, it stores a &str only. +/// See [IdBuf](crate::model::idtypes::IdBuf) for owned version of the type. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Id<'id, T> { + _type: PhantomData, + id: &'id str, +} + +impl<'id, T> Id<'id, T> { + pub fn to_owned(&self) -> IdBuf { + IdBuf { + _type: PhantomData, + id: self.id.to_owned(), + } + } +} + +/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is an owning type, it stores a String. +/// See [IdBuf](crate::model::idtypes::Id) for light-weight non-owning type. +/// +/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or `Id::from_id_or_uri(val).to_owned()` +/// to construct an instance of this type. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct IdBuf { + _type: PhantomData, + id: String, +} + +impl<'id, T> Into> for &'id IdBuf { + fn into(self) -> Id<'id, T> { + Id { + _type: PhantomData, + id: &self.id, + } + } +} + +impl IdBuf { + /// Get a non-owning [`Id`](crate::model::idtypes::Id) representation of the id + pub fn as_ref(&self) -> Id<'_, T> { + self.into() + } + + /// Get a [`Type`](crate::model::enums::types::Type) of the id + pub fn _type(&self) -> Type { + T::TYPE + } + + /// Get id value as a &str + pub fn id(&self) -> &str { + &self.id + } +} + +/// Spotify id or URI parsing error +/// +/// See also [`Id`](crate::model::idtypes::Id) for details. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] +pub enum IdError { + /// 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) + 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) + InvalidType, + /// Spotify id is invalid (empty or contains non-alphanumeric characters) + InvalidId, +} + +impl std::fmt::Display for Id<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "spotify:{}:{}", T::TYPE, self.id) + } +} + +impl AsRef for Id<'_, T> { + fn as_ref(&self) -> &str { + self.id + } +} + +impl std::str::FromStr for IdBuf { + type Err = IdError; + + fn from_str(s: &str) -> Result { + Id::from_id_or_uri(s).map(|id| id.to_owned()) + } +} + +impl Id<'_, T> { + /// Spotify object type + pub fn _type(&self) -> Type { + T::TYPE + } + + /// Spotify object id (guaranteed to be a string of alphanumeric characters) + pub fn id(&self) -> &str { + self.id + } + + /// Spotify object URI in a well-known format: spotify:type:id + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. + pub fn uri(&self) -> String { + format!("spotify:{}:{}", T::TYPE, self.id) + } + + /// Full Spotify object URL, can be opened in a browser + /// + /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI + pub fn url(&self) -> String { + format!("https://open.spotify.com/{}/{}", T::TYPE, self.id) + } + + /// 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 alphanumeric string. + /// The URI must be of given `T`ype, 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 alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part is not equal to `_type`, + /// - `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 non-alphanumeric characters), + /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. + pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result, IdError> { + match Id::::from_uri(id_or_uri) { + Ok(id) => Ok(id), + Err(IdError::InvalidPrefix) => Id::::from_id(id_or_uri), + Err(error) => Err(error), + } + } + + /// Parse Spotify id from string slice + /// + /// A valid Spotify object id must be a non-empty alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. + pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result, IdError> { + if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { + Ok(Id { + _type: PhantomData, + 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 alphanumeric 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. + pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { + let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; + let sep = match rest.chars().next() { + Some(ch) if ch == '/' || ch == ':' => ch, + _ => return Err(IdError::InvalidPrefix), + }; + let rest = &rest[1..]; + + if let Some((tpe, id)) = rest.rfind(sep).map(|mid| rest.split_at(mid)) { + let _type: Type = tpe.parse().map_err(|_| IdError::InvalidType)?; + if _type != T::TYPE { + return Err(IdError::InvalidType); + } + Id::::from_id(&id[1..]) + } else { + Err(IdError::InvalidFormat) + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 07d68252..d9cc4fac 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -6,6 +6,7 @@ pub mod category; pub mod context; pub mod device; pub mod enums; +pub mod idtypes; pub mod image; pub mod offset; pub mod page; @@ -20,10 +21,6 @@ pub mod user; use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{de, Deserialize, Serialize, Serializer}; use std::{fmt, time::Duration}; -use strum::Display; -use thiserror::Error; - -use self::enums::idtypes::IdType; /// Vistor to help deserialize duration represented as millisecond to `std::time::Duration` struct DurationVisitor; @@ -172,213 +169,31 @@ pub enum PlayingItem { Episode(show::FullEpisode), } -/// A Spotify object id of given [type](crate::model::enums::types::Type) -/// -/// This is a not-owning type, it stores a &str only. -/// See [IdBuf](crate::model::IdBuf) for owned version of the type. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct Id<'id, T> { - _type: PhantomData, - id: &'id str, -} - -impl<'id, T> Id<'id, T> { - pub fn to_owned(&self) -> IdBuf { - IdBuf { - _type: PhantomData, - id: self.id.to_owned(), - } - } -} - -/// A Spotify object id of given [type](crate::model::enums::types::Type) -/// -/// This is an owning type, it stores a String. -/// See [IdBuf](crate::model::Id) for light-weight non-owning type. -/// -/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned` or `Id::from_id_or_uri(val).to_owned()` -/// to construct an instance of this type. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct IdBuf { - _type: PhantomData, - id: String, -} - -impl<'id, T> Into> for &'id IdBuf { - fn into(self) -> Id<'id, T> { - Id { - _type: PhantomData, - id: &self.id, - } - } -} - -impl IdBuf { - /// Get a non-owning [`Id`] representation of the id - pub fn as_ref(&self) -> Id<'_, T> { - self.into() - } - - /// Get a [`Type`](crate::model::enums::types::Type) of the id - pub fn _type(&self) -> Type { - T::TYPE - } - - /// Get id value as a &str - pub fn id(&self) -> &str { - &self.id - } -} - -/// Spotify id or URI parsing error -/// -/// See also [`Id`](crate::model::Id) for details. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] -pub enum IdError { - /// 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) - 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) - InvalidType, - /// Spotify id is invalid (empty or contains non-alphanumeric characters) - InvalidId, -} - -impl std::fmt::Display for Id<'_, T> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "spotify:{}:{}", T::TYPE, self.id) - } -} - -impl AsRef for Id<'_, T> { - fn as_ref(&self) -> &str { - &self.id - } -} - -impl std::str::FromStr for IdBuf { - type Err = IdError; - - fn from_str(s: &str) -> Result { - Id::from_id_or_uri(s).map(|id| id.to_owned()) - } -} - -impl Id<'_, T> { - /// Spotify object type - pub fn _type(&self) -> Type { - T::TYPE - } - - /// Spotify object id (guaranteed to be a string of alphanumeric characters) - pub fn id(&self) -> &str { - self.id - } - - /// Spotify object URI in a well-known format: spotify:type:id - /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. - pub fn uri(&self) -> String { - format!("spotify:{}:{}", T::TYPE, &self.id) - } - - /// Full Spotify object URL, can be opened in a browser - /// - /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI - pub fn url(&self) -> String { - format!("https://open.spotify.com/{}/{}", T::TYPE, &self.id) - } - - /// 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 alphanumeric string. - /// The URI must be of given `T`ype, 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 alphanumeric string. - /// - /// # Errors: - /// - /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part is not equal to `_type`, - /// - `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 non-alphanumeric characters), - /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. - pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result, IdError> { - match Id::::from_uri(id_or_uri) { - Ok(id) => Ok(id), - Err(IdError::InvalidPrefix) => Id::::from_id(id_or_uri), - Err(error) => Err(error), - } - } - - /// Parse Spotify id from string slice - /// - /// A valid Spotify object id must be a non-empty alphanumeric string. - /// - /// # Errors: - /// - /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. - pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result, IdError> { - if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { - Ok(Id { - _type: PhantomData, - 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 alphanumeric 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. - pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { - let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; - let sep = match rest.chars().next() { - Some(ch) if ch == '/' || ch == ':' => ch, - _ => return Err(IdError::InvalidPrefix), - }; - let rest = &rest[1..]; - - if let Some((tpe, id)) = rest.rfind(sep).map(|mid| rest.split_at(mid)) { - let _type: Type = tpe.parse().map_err(|_| IdError::InvalidType)?; - if _type != T::TYPE { - return Err(IdError::InvalidType); - } - Id::::from_id(&id[1..]) - } else { - Err(IdError::InvalidFormat) - } - } -} - -use std::marker::PhantomData; pub use { - album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, - offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, + album::*, + artist::*, + audio::*, + category::*, + context::*, + device::*, + enums::*, + idtypes::{Id, IdBuf, IdError}, + image::*, + offset::*, + page::*, + playing::*, + playlist::*, + recommend::*, + search::*, + show::*, + track::*, user::*, }; #[cfg(test)] mod tests { use super::*; + use crate::model::{Id, IdError}; #[test] fn test_get_id() { diff --git a/src/model/track.rs b/src/model/track.rs index 42f50850..197521cf 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -9,6 +9,7 @@ use super::artist::SimplifiedArtist; use super::Restriction; use crate::model::{from_duration_ms, to_duration_ms}; use crate::model::{idtypes, Id, Type}; + /// Full track object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-full) From 1197dbac43a00cdced9bbb698065e2813757ede0 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 00:34:54 +0300 Subject: [PATCH 28/59] simplify from_uri() --- src/model/idtypes.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 32e95275..dd17bff2 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -238,16 +238,17 @@ impl Id<'_, T> { Some(ch) if ch == '/' || ch == ':' => ch, _ => return Err(IdError::InvalidPrefix), }; - let rest = &rest[1..]; - - if let Some((tpe, id)) = rest.rfind(sep).map(|mid| rest.split_at(mid)) { - let _type: Type = tpe.parse().map_err(|_| IdError::InvalidType)?; - if _type != T::TYPE { - return Err(IdError::InvalidType); - } - Id::::from_id(&id[1..]) - } else { - Err(IdError::InvalidFormat) + // It's safe to do .get_unchecked() because we checked the first char above + let rest = unsafe { rest.get_unchecked(1..) }; + + let (tpe, id) = rest + .rfind(sep) + .map(|mid| rest.split_at(mid)) + .ok_or(IdError::InvalidFormat)?; + + match tpe.parse::() { + Ok(tpe) if tpe == T::TYPE => Id::::from_id(&id[1..]), + _ => Err(IdError::InvalidType), } } } From e67126cecc15bc7c2e7979c393677852ba51d929 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 00:53:15 +0300 Subject: [PATCH 29/59] helper id type aliases --- src/model/idtypes.rs | 8 ++++++++ src/model/mod.rs | 22 +++++----------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index dd17bff2..91f7eb2d 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -33,6 +33,14 @@ impl IdType for Episode { const TYPE: Type = Type::Episode; } +pub type ArtistId<'a> = Id<'a, Artist>; +pub type AlbumId<'a> = Id<'a, Album>; +pub type TrackId<'a> = Id<'a, Track>; +pub type PlaylistId<'a> = Id<'a, Playlist>; +pub type UserId<'a> = Id<'a, User>; +pub type ShowId<'a> = Id<'a, Show>; +pub type EpisodeId<'a> = Id<'a, Episode>; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Artist {} impl private::Sealed for Artist {} diff --git a/src/model/mod.rs b/src/model/mod.rs index d9cc4fac..d8220539 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -169,24 +169,12 @@ pub enum PlayingItem { Episode(show::FullEpisode), } +pub use idtypes::{ + AlbumId, ArtistId, EpisodeId, Id, IdBuf, IdError, PlaylistId, ShowId, TrackId, UserId, +}; pub use { - album::*, - artist::*, - audio::*, - category::*, - context::*, - device::*, - enums::*, - idtypes::{Id, IdBuf, IdError}, - image::*, - offset::*, - page::*, - playing::*, - playlist::*, - recommend::*, - search::*, - show::*, - track::*, + album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, + offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, user::*, }; From e4dc911263fc4b436bd64f42ec31cb3be09bbfc7 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 01:03:16 +0300 Subject: [PATCH 30/59] use id types aliases --- src/client.rs | 88 +++++++++++++++++++++------------------------- src/model/track.rs | 7 ++-- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3cf17488..668aec33 100644 --- a/src/client.rs +++ b/src/client.rs @@ -189,7 +189,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-track/) #[maybe_async] - pub async fn track(&self, track_id: Id<'_, idtypes::Track>) -> ClientResult { + pub async fn track(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("tracks/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -205,7 +205,7 @@ impl Spotify { #[maybe_async] pub async fn tracks<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, market: Option, ) -> ClientResult> { let ids = track_ids.into_iter().join(","); @@ -227,7 +227,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-artist/) #[maybe_async] - pub async fn artist(&self, artist_id: Id<'_, idtypes::Artist>) -> ClientResult { + pub async fn artist(&self, artist_id: ArtistId<'_>) -> ClientResult { let url = format!("artists/{}", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -242,7 +242,7 @@ impl Spotify { #[maybe_async] pub async fn artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult> { let ids = artist_ids.into_iter().join(","); let url = format!("artists/?ids={}", ids); @@ -265,7 +265,7 @@ impl Spotify { #[maybe_async] pub async fn artist_albums( &self, - artist_id: Id<'_, idtypes::Artist>, + artist_id: ArtistId<'_>, album_type: Option, country: Option, limit: Option, @@ -300,7 +300,7 @@ impl Spotify { #[maybe_async] pub async fn artist_top_tracks>>( &self, - artist_id: Id<'_, idtypes::Artist>, + artist_id: ArtistId<'_>, country: T, ) -> ClientResult> { let mut params = Query::with_capacity(1); @@ -325,7 +325,7 @@ impl Spotify { #[maybe_async] pub async fn artist_related_artists( &self, - artist_id: Id<'_, idtypes::Artist>, + artist_id: ArtistId<'_>, ) -> ClientResult> { let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; @@ -340,7 +340,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-album/) #[maybe_async] - pub async fn album(&self, album_id: Id<'_, idtypes::Album>) -> ClientResult { + pub async fn album(&self, album_id: AlbumId<'_>) -> ClientResult { let url = format!("albums/{}", album_id.id()); let result = self.get(&url, None, &Query::new()).await?; @@ -356,7 +356,7 @@ impl Spotify { #[maybe_async] pub async fn albums<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult> { let ids = album_ids.into_iter().join(","); let url = format!("albums/?ids={}", ids); @@ -416,7 +416,7 @@ impl Spotify { #[maybe_async] pub async fn album_track>, O: Into>>( &self, - album_id: Id<'_, idtypes::Album>, + album_id: AlbumId<'_>, limit: L, offset: O, ) -> ClientResult> { @@ -435,7 +435,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-users-profile/) #[maybe_async] - pub async fn user(&self, user_id: Id<'_, idtypes::User>) -> ClientResult { + pub async fn user(&self, user_id: UserId<'_>) -> ClientResult { let url = format!("users/{}", user_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -451,7 +451,7 @@ impl Spotify { #[maybe_async] pub async fn playlist( &self, - playlist_id: Id<'_, idtypes::Playlist>, + playlist_id: PlaylistId<'_>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -524,7 +524,7 @@ impl Spotify { pub async fn user_playlist( &self, user_id: &str, - playlist_id: Option>, + playlist_id: Option>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -562,7 +562,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_tracks>, O: Into>>( &self, - playlist_id: Id<'_, idtypes::Playlist>, + playlist_id: PlaylistId<'_>, fields: Option<&str>, limit: L, offset: O, @@ -670,8 +670,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_add_tracks<'a>( &self, - playlist_id: Id<'_, idtypes::Playlist>, - track_ids: impl IntoIterator>, + playlist_id: PlaylistId<'_>, + track_ids: impl IntoIterator>, position: Option, ) -> ClientResult { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -696,8 +696,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_replace_tracks<'a>( &self, - playlist_id: Id<'_, idtypes::Playlist>, - track_ids: impl IntoIterator>, + playlist_id: PlaylistId<'_>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -721,7 +721,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_reorder_tracks>>( &self, - playlist_id: Id<'_, idtypes::Playlist>, + playlist_id: PlaylistId<'_>, range_start: i32, range_length: R, insert_before: i32, @@ -752,8 +752,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, - playlist_id: Id<'_, idtypes::Playlist>, - track_ids: impl IntoIterator>, + playlist_id: PlaylistId<'_>, + track_ids: impl IntoIterator>, snapshot_id: Option, ) -> ClientResult { let tracks = track_ids @@ -808,7 +808,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_specific_occurrences_of_tracks( &self, - playlist_id: Id<'_, idtypes::Playlist>, + playlist_id: PlaylistId<'_>, tracks: Vec>, snapshot_id: Option, ) -> ClientResult { @@ -998,7 +998,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/?ids={}", ids); @@ -1017,7 +1017,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult> { let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/contains/?ids={}", ids); @@ -1034,7 +1034,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = track_ids.into_iter().join(","); let url = format!("me/tracks/?ids={}", ids); @@ -1135,7 +1135,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = album_ids.into_iter().join(","); let url = format!("me/albums/?ids={}", ids); @@ -1153,7 +1153,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = album_ids.into_iter().join(","); let url = format!("me/albums/?ids={}", ids); @@ -1172,7 +1172,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator>, ) -> ClientResult> { let ids = album_ids.into_iter().join(","); let url = format!("me/albums/contains/?ids={}", ids); @@ -1189,7 +1189,7 @@ impl Spotify { #[maybe_async] pub async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = artist_ids.into_iter().join(","); let url = format!("me/following?type=artist&ids={}", ids); @@ -1207,7 +1207,7 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = artist_ids.into_iter().join(","); let url = format!("me/following?type=artist&ids={}", ids); @@ -1226,7 +1226,7 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator>, ) -> ClientResult> { let ids = artist_ids.into_iter().join(","); let url = format!("me/following/contains?type=artist&ids={}", ids); @@ -1243,7 +1243,7 @@ impl Spotify { #[maybe_async] pub async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = user_ids.into_iter().join(","); let url = format!("me/following?type=user&ids={}", ids); @@ -1261,7 +1261,7 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator>, ) -> ClientResult<()> { let ids = user_ids.into_iter().join(","); let url = format!("me/following?type=user&ids={}", ids); @@ -1425,9 +1425,9 @@ impl Spotify { #[maybe_async] pub async fn recommendations>>( &self, - seed_artists: Option>>, + seed_artists: Option>>, seed_genres: Option>, - seed_tracks: Option>>, + seed_tracks: Option>>, limit: L, country: Option, payload: &Map, @@ -1492,10 +1492,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-features/) #[maybe_async] - pub async fn track_features( - &self, - track_id: Id<'_, idtypes::Track>, - ) -> ClientResult { + pub async fn track_features(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("audio-features/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1510,7 +1507,7 @@ impl Spotify { #[maybe_async] pub async fn tracks_features<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator>, ) -> ClientResult>> { let ids = track_ids.into_iter().join(","); let url = format!("audio-features/?ids={}", ids); @@ -1531,10 +1528,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-analysis/) #[maybe_async] - pub async fn track_analysis( - &self, - track_id: Id<'_, idtypes::Track>, - ) -> ClientResult { + pub async fn track_analysis(&self, track_id: TrackId<'_>) -> ClientResult { let url = format!("audio-analysis/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1894,7 +1888,7 @@ impl Spotify { #[maybe_async] pub async fn get_a_show( &self, - id: Id<'_, idtypes::Show>, + id: ShowId<'_>, market: Option, ) -> ClientResult { let mut params = Query::new(); @@ -1949,7 +1943,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: Id<'_, idtypes::Show>, + id: ShowId<'_>, limit: L, offset: O, market: Option, @@ -1977,7 +1971,7 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: Id<'_, idtypes::Episode>, + id: EpisodeId<'_>, market: Option, ) -> ClientResult { let url = format!("episodes/{}", id.id()); diff --git a/src/model/track.rs b/src/model/track.rs index 197521cf..9b27cb2f 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -7,8 +7,7 @@ use std::{collections::HashMap, time::Duration}; use super::album::SimplifiedAlbum; use super::artist::SimplifiedArtist; use super::Restriction; -use crate::model::{from_duration_ms, to_duration_ms}; -use crate::model::{idtypes, Id, Type}; +use crate::model::{from_duration_ms, to_duration_ms, TrackId, Type}; /// Full track object /// @@ -113,13 +112,13 @@ pub struct SavedTrack { /// Track id with specific positions track in a playlist pub struct TrackPositions<'id> { - pub id: Id<'id, idtypes::Track>, + pub id: TrackId<'id>, pub positions: Vec, } impl<'id> TrackPositions<'id> { /// Track in a playlist by an id - pub fn new(id: Id<'id, idtypes::Track>, positions: Vec) -> Self { + pub fn new(id: TrackId<'id>, positions: Vec) -> Self { Self { id, positions } } } From b31d600a26877911a6e525f0dc2eea5ea50eb56d Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 10:23:23 +0300 Subject: [PATCH 31/59] add owned aliases for ids --- src/model/idtypes.rs | 8 ++++++++ src/model/mod.rs | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 91f7eb2d..265b9842 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -41,6 +41,14 @@ pub type UserId<'a> = Id<'a, User>; pub type ShowId<'a> = Id<'a, Show>; pub type EpisodeId<'a> = Id<'a, Episode>; +pub type ArtistIdBuf = IdBuf; +pub type AlbumIdBuf = IdBuf; +pub type TrackIdBuf = IdBuf; +pub type PlaylistIdBuf = IdBuf; +pub type UserIdBuf = IdBuf; +pub type ShowIdBuf = IdBuf; +pub type EpisodeIdBuf = IdBuf; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Artist {} impl private::Sealed for Artist {} diff --git a/src/model/mod.rs b/src/model/mod.rs index d8220539..f459dc19 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -170,7 +170,8 @@ pub enum PlayingItem { } pub use idtypes::{ - AlbumId, ArtistId, EpisodeId, Id, IdBuf, IdError, PlaylistId, ShowId, TrackId, UserId, + AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, + PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, UserIdBuf, }; pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, From 458e729800d1c795b88c7b92abbf53494fe26b85 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 25 Dec 2020 10:53:34 +0300 Subject: [PATCH 32/59] more methods and better conversions for idbuf/id --- src/model/idtypes.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 265b9842..a60c070a 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -81,15 +81,6 @@ pub struct Id<'id, T> { id: &'id str, } -impl<'id, T> Id<'id, T> { - pub fn to_owned(&self) -> IdBuf { - IdBuf { - _type: PhantomData, - id: self.id.to_owned(), - } - } -} - /// A Spotify object id of given [type](crate::model::enums::types::Type) /// /// This is an owning type, it stores a String. @@ -123,10 +114,20 @@ impl IdBuf { T::TYPE } - /// Get id value as a &str + /// Spotify object id (guaranteed to be a string of alphanumeric characters) pub fn id(&self) -> &str { &self.id } + + /// Spotify object URI in a well-known format: spotify:type:id + pub fn uri(&self) -> String { + self.as_ref().uri() + } + + /// Full Spotify object URL, can be opened in a browser + pub fn url(&self) -> String { + self.as_ref().url() + } } /// Spotify id or URI parsing error @@ -157,6 +158,15 @@ impl AsRef for Id<'_, T> { } } +impl Into> for &Id<'_, T> { + fn into(self) -> IdBuf { + IdBuf { + _type: PhantomData, + id: self.id.to_owned(), + } + } +} + impl std::str::FromStr for IdBuf { type Err = IdError; @@ -166,6 +176,11 @@ impl std::str::FromStr for IdBuf { } impl Id<'_, T> { + /// Owned version of the id [`IdBuf`](crate::model::idtypes::IdBuf) + pub fn to_owned(&self) -> IdBuf { + self.into() + } + /// Spotify object type pub fn _type(&self) -> Type { T::TYPE From 77e1d47522ba0ef7f9230fe4b5c11e618ce0d8d0 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 13:10:42 +0300 Subject: [PATCH 33/59] show/episode methods --- src/client.rs | 37 +++++++++++++++---------------------- src/model/idtypes.rs | 25 +++++++++++++++++++++++++ src/model/mod.rs | 3 ++- tests/test_with_oauth.rs | 11 ++++++++--- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/client.rs b/src/client.rs index 668aec33..fce976b3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1831,10 +1831,10 @@ impl Spotify { #[maybe_async] pub async fn add_item_to_queue( &self, - item: String, + item: impl Into>, device_id: Option, ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", &item), device_id); + let url = self.append_device_id(&format!("me/player/queue?uri={}", item.into()), device_id); self.post(&url, None, &json!({})).await?; Ok(()) @@ -1847,8 +1847,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/console/put-current-user-saved-shows) #[maybe_async] - pub async fn save_shows<'a>(&self, ids: impl IntoIterator) -> ClientResult<()> { - let joined_ids = ids.into_iter().collect::>().join(","); + pub async fn save_shows<'a>( + &self, + ids: impl IntoIterator>, + ) -> ClientResult<()> { + let joined_ids = ids.into_iter().join(","); let url = format!("me/shows/?ids={}", joined_ids); self.put(&url, None, &json!({})).await?; @@ -1911,15 +1914,11 @@ impl Spotify { #[maybe_async] pub async fn get_several_shows<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator>, market: Option, ) -> ClientResult> { - // TODO: This can probably be better let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), ids.into_iter().join(",")); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } @@ -1994,14 +1993,11 @@ impl Spotify { #[maybe_async] pub async fn get_several_episodes<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator>, market: Option, ) -> ClientResult { let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), ids.into_iter().join(",")); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } @@ -2018,13 +2014,10 @@ impl Spotify { #[maybe_async] pub async fn check_users_saved_shows<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator>, ) -> ClientResult> { let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), ids.into_iter().join(",")); let result = self.get("me/shows/contains", None, ¶ms).await?; self.convert_result(&result) } @@ -2040,10 +2033,10 @@ impl Spotify { #[maybe_async] pub async fn remove_users_saved_shows<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator>, market: Option, ) -> ClientResult<()> { - let joined_ids = ids.into_iter().collect::>().join(","); + let joined_ids = ids.into_iter().join(","); let url = format!("me/shows?ids={}", joined_ids); let mut params = json!({}); if let Some(market) = market { diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index a60c070a..ca5df183 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -283,3 +283,28 @@ impl Id<'_, T> { } } } + +pub enum PlayableId<'id> { + Episode(EpisodeId<'id>), + Track(TrackId<'id>), +} + +impl<'id> From> for PlayableId<'id> { + fn from(id: EpisodeId<'id>) -> Self { + PlayableId::Episode(id) + } +} +impl<'id> From> for PlayableId<'id> { + fn from(id: TrackId<'id>) -> Self { + PlayableId::Track(id) + } +} + +impl<'id> std::fmt::Display for PlayableId<'id> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + PlayableId::Episode(id) => id.fmt(f), + PlayableId::Track(id) => id.fmt(f), + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index f459dc19..8e1c2b64 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -171,7 +171,8 @@ pub enum PlayingItem { pub use idtypes::{ AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, - PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, UserIdBuf, + PlayableId, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, + UserIdBuf, }; pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 2da2270d..b52ddb28 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -19,7 +19,9 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; -use rspotify::model::{Country, Id, RepeatState, SearchType, TimeRange, TrackPositions}; +use rspotify::model::{ + Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, +}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use std::env; @@ -761,7 +763,7 @@ async fn test_volume() { #[maybe_async_test] #[ignore] async fn test_add_queue() { - let birdy_uri = String::from("spotify:track:6rqhFgbbKwnb9MLmUQDhG6"); + let birdy_uri = TrackId::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); oauth_client() .await .add_item_to_queue(birdy_uri, None) @@ -776,7 +778,10 @@ async fn test_get_several_shows() { oauth_client() .await .get_several_shows( - vec!["5CfCWKI5pZ28U0uOzXkDHe", "5as3aKmN2k11yfDDDSrvaZ"], + vec![ + ShowId::from_id("5CfCWKI5pZ28U0uOzXkDHe").unwrap(), + ShowId::from_id("5as3aKmN2k11yfDDDSrvaZ").unwrap(), + ], None, ) .await From c8776fe8059d378d6dd66d372f8966b3394c57d2 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 13:19:35 +0300 Subject: [PATCH 34/59] user methods use id types --- src/client.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index fce976b3..316f8e38 100644 --- a/src/client.rs +++ b/src/client.rs @@ -500,14 +500,14 @@ impl Spotify { #[maybe_async] pub async fn user_playlists>, O: Into>>( &self, - user_id: &str, + user_id: UserId<'_>, limit: L, offset: O, ) -> ClientResult> { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let url = format!("users/{}/playlists", user_id); + let url = format!("users/{}/playlists", user_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -523,7 +523,7 @@ impl Spotify { #[maybe_async] pub async fn user_playlist( &self, - user_id: &str, + user_id: UserId<'_>, playlist_id: Option>, fields: Option<&str>, market: Option, @@ -537,12 +537,12 @@ impl Spotify { } match playlist_id { Some(playlist_id) => { - let url = format!("users/{}/playlists/{}", user_id, playlist_id.id()); + let url = format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } None => { - let url = format!("users/{}/starred", user_id); + let url = format!("users/{}/starred", user_id.id()); let result = self.get(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -594,7 +594,7 @@ impl Spotify { #[maybe_async] pub async fn user_playlist_create>, D: Into>>( &self, - user_id: &str, + user_id: UserId<'_>, name: &str, public: P, description: D, @@ -606,7 +606,7 @@ impl Spotify { "public": public, "description": description }); - let url = format!("users/{}/playlists", user_id); + let url = format!("users/{}/playlists", user_id.id()); let result = self.post(&url, None, ¶ms).await?; self.convert_result(&result) } @@ -866,10 +866,10 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/check-user-following-playlist/) #[maybe_async] - pub async fn playlist_check_follow( + pub async fn playlist_check_follow<'a>( &self, - playlist_id: &str, - user_ids: &[String], + playlist_id: PlaylistId<'_>, + user_ids: impl IntoIterator>, ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); From 375f181cbee49be47436326bafe980122b58d9b6 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 15:14:14 +0300 Subject: [PATCH 35/59] remove Join trait, update tests, playable id type marker --- src/client.rs | 135 +++++++++++++++------------------------ src/model/idtypes.rs | 28 +------- src/model/mod.rs | 2 +- tests/test_with_oauth.rs | 16 ++--- 4 files changed, 64 insertions(+), 117 deletions(-) diff --git a/src/client.rs b/src/client.rs index 316f8e38..2d65f516 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,34 +15,7 @@ use super::http::{BaseClient, Query}; use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; - -pub trait Join: Iterator { - fn join(&mut self, sep: &str) -> String - where - Self: Iterator, - Self::Item: AsRef, - { - if let Some(item) = self.next() { - let value = item.as_ref(); - let (size, _) = self.size_hint(); - let cap = size * (sep.len() + value.len()); - - let mut output = String::with_capacity(cap); - output.push_str(value); - - for item in self { - output.push_str(sep); - output.push_str(item.as_ref()); - } - - output - } else { - String::new() - } - } -} - -impl Join for T where T: Iterator {} +use crate::model::idtypes::IdType; /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] @@ -208,7 +181,7 @@ impl Spotify { track_ids: impl IntoIterator>, market: Option, ) -> ClientResult> { - let ids = track_ids.into_iter().join(","); + let ids = join_ids(track_ids); let mut params = Query::new(); if let Some(market) = market { @@ -244,7 +217,7 @@ impl Spotify { &self, artist_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = artist_ids.into_iter().join(","); + let ids = join_ids(artist_ids); let url = format!("artists/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; @@ -358,7 +331,7 @@ impl Spotify { &self, album_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = album_ids.into_iter().join(","); + let ids = join_ids(album_ids); let url = format!("albums/?ids={}", ids); let result = self.get(&url, None, &Query::new()).await?; self.convert_result::(&result).map(|x| x.albums) @@ -840,10 +813,10 @@ impl Spotify { #[maybe_async] pub async fn playlist_follow>>( &self, - playlist_id: &str, + playlist_id: PlaylistId<'_>, public: P, ) -> ClientResult<()> { - let url = format!("playlists/{}/followers", playlist_id); + let url = format!("playlists/{}/followers", playlist_id.id()); self.put( &url, @@ -869,15 +842,19 @@ impl Spotify { pub async fn playlist_check_follow<'a>( &self, playlist_id: PlaylistId<'_>, - user_ids: impl IntoIterator>, + user_ids: &[UserId<'_>], ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); } let url = format!( "playlists/{}/followers/contains?ids={}", - playlist_id, - user_ids.join(",") + playlist_id.id(), + user_ids + .iter() + .map(|id| id.id()) + .collect::>() + .join(","), ); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1000,8 +977,7 @@ impl Spotify { &self, track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = track_ids.into_iter().join(","); - let url = format!("me/tracks/?ids={}", ids); + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1019,8 +995,7 @@ impl Spotify { &self, track_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = track_ids.into_iter().join(","); - let url = format!("me/tracks/contains/?ids={}", ids); + let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1036,8 +1011,7 @@ impl Spotify { &self, track_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = track_ids.into_iter().join(","); - let url = format!("me/tracks/?ids={}", ids); + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1137,8 +1111,7 @@ impl Spotify { &self, album_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = album_ids.into_iter().join(","); - let url = format!("me/albums/?ids={}", ids); + let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1155,8 +1128,7 @@ impl Spotify { &self, album_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = album_ids.into_iter().join(","); - let url = format!("me/albums/?ids={}", ids); + let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1174,8 +1146,7 @@ impl Spotify { &self, album_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = album_ids.into_iter().join(","); - let url = format!("me/albums/contains/?ids={}", ids); + let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1191,8 +1162,7 @@ impl Spotify { &self, artist_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = artist_ids.into_iter().join(","); - let url = format!("me/following?type=artist&ids={}", ids); + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1209,8 +1179,7 @@ impl Spotify { &self, artist_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = artist_ids.into_iter().join(","); - let url = format!("me/following?type=artist&ids={}", ids); + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1228,8 +1197,10 @@ impl Spotify { &self, artist_ids: impl IntoIterator>, ) -> ClientResult> { - let ids = artist_ids.into_iter().join(","); - let url = format!("me/following/contains?type=artist&ids={}", ids); + let url = format!( + "me/following/contains?type=artist&ids={}", + join_ids(artist_ids) + ); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) } @@ -1245,8 +1216,7 @@ impl Spotify { &self, user_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = user_ids.into_iter().join(","); - let url = format!("me/following?type=user&ids={}", ids); + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1263,8 +1233,7 @@ impl Spotify { &self, user_ids: impl IntoIterator>, ) -> ClientResult<()> { - let ids = user_ids.into_iter().join(","); - let url = format!("me/following?type=user&ids={}", ids); + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.delete(&url, None, &json!({})).await?; Ok(()) @@ -1464,8 +1433,7 @@ impl Spotify { } if let Some(seed_artists) = seed_artists { - let seed_artists_ids = seed_artists.into_iter().join(","); - params.insert("seed_artists".to_owned(), seed_artists_ids); + params.insert("seed_artists".to_owned(), join_ids(seed_artists)); } if let Some(seed_genres) = seed_genres { @@ -1473,8 +1441,7 @@ impl Spotify { } if let Some(seed_tracks) = seed_tracks { - let seed_tracks_ids = seed_tracks.into_iter().join(","); - params.insert("seed_tracks".to_owned(), seed_tracks_ids); + params.insert("seed_tracks".to_owned(), join_ids(seed_tracks)); } if let Some(country) = country { @@ -1509,8 +1476,7 @@ impl Spotify { &self, track_ids: impl IntoIterator>, ) -> ClientResult>> { - let ids = track_ids.into_iter().join(","); - let url = format!("audio-features/?ids={}", ids); + let url = format!("audio-features/?ids={}", join_ids(track_ids)); let result = self.get(&url, None, &Query::new()).await?; if result.is_empty() { @@ -1829,12 +1795,12 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/console/post-queue/) #[maybe_async] - pub async fn add_item_to_queue( + pub async fn add_item_to_queue( &self, - item: impl Into>, + item: Id<'_, T>, device_id: Option, ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", item.into()), device_id); + let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); self.post(&url, None, &json!({})).await?; Ok(()) @@ -1849,10 +1815,9 @@ impl Spotify { #[maybe_async] pub async fn save_shows<'a>( &self, - ids: impl IntoIterator>, + show_ids: impl IntoIterator>, ) -> ClientResult<()> { - let joined_ids = ids.into_iter().join(","); - let url = format!("me/shows/?ids={}", joined_ids); + let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.put(&url, None, &json!({})).await?; Ok(()) @@ -1918,7 +1883,7 @@ impl Spotify { market: Option, ) -> ClientResult> { let mut params = Query::with_capacity(1); - params.insert("ids".to_owned(), ids.into_iter().join(",")); + params.insert("ids".to_owned(), join_ids(ids)); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } @@ -1997,7 +1962,7 @@ impl Spotify { market: Option, ) -> ClientResult { let mut params = Query::with_capacity(1); - params.insert("ids".to_owned(), ids.into_iter().join(",")); + params.insert("ids".to_owned(), join_ids(ids)); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); } @@ -2017,7 +1982,7 @@ impl Spotify { ids: impl IntoIterator>, ) -> ClientResult> { let mut params = Query::with_capacity(1); - params.insert("ids".to_owned(), ids.into_iter().join(",")); + params.insert("ids".to_owned(), join_ids(ids)); let result = self.get("me/shows/contains", None, ¶ms).await?; self.convert_result(&result) } @@ -2033,11 +1998,10 @@ impl Spotify { #[maybe_async] pub async fn remove_users_saved_shows<'a>( &self, - ids: impl IntoIterator>, + show_ids: impl IntoIterator>, market: Option, ) -> ClientResult<()> { - let joined_ids = ids.into_iter().join(","); - let url = format!("me/shows?ids={}", joined_ids); + let url = format!("me/shows?ids={}", join_ids(show_ids)); let mut params = json!({}); if let Some(market) = market { json_insert!(params, "country", market.to_string()); @@ -2048,6 +2012,18 @@ impl Spotify { } } +fn join_ids<'a, T: IdType>(ids: impl IntoIterator>) -> String { + let mut out = String::new(); + let mut iter = ids.into_iter(); + if let Some(first) = iter.next() { + out += first.id(); + for id in iter { + out = out + "," + id.id(); + } + } + out +} + #[cfg(test)] mod tests { use super::*; @@ -2059,11 +2035,4 @@ mod tests { let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } - - #[test] - fn test_join() { - let data = vec!["a", "b", "c"]; - let joined = data.into_iter().join(","); - assert_eq!("a,b,c", &joined); - } } diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index ca5df183..6d7be522 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -10,6 +10,7 @@ mod private { pub trait IdType: private::Sealed { const TYPE: Type; } +pub trait PlayableIdType: IdType {} impl IdType for Artist { const TYPE: Type = Type::Artist; @@ -20,6 +21,7 @@ impl IdType for Album { impl IdType for Track { const TYPE: Type = Type::Track; } +impl PlayableIdType for Track {} impl IdType for Playlist { const TYPE: Type = Type::Playlist; } @@ -32,6 +34,7 @@ impl IdType for Show { impl IdType for Episode { const TYPE: Type = Type::Episode; } +impl PlayableIdType for Episode {} pub type ArtistId<'a> = Id<'a, Artist>; pub type AlbumId<'a> = Id<'a, Album>; @@ -283,28 +286,3 @@ impl Id<'_, T> { } } } - -pub enum PlayableId<'id> { - Episode(EpisodeId<'id>), - Track(TrackId<'id>), -} - -impl<'id> From> for PlayableId<'id> { - fn from(id: EpisodeId<'id>) -> Self { - PlayableId::Episode(id) - } -} -impl<'id> From> for PlayableId<'id> { - fn from(id: TrackId<'id>) -> Self { - PlayableId::Track(id) - } -} - -impl<'id> std::fmt::Display for PlayableId<'id> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - PlayableId::Episode(id) => id.fmt(f), - PlayableId::Track(id) => id.fmt(f), - } - } -} diff --git a/src/model/mod.rs b/src/model/mod.rs index 8e1c2b64..45a12ed2 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -171,7 +171,7 @@ pub enum PlayingItem { pub use idtypes::{ AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, - PlayableId, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, + PlayableIdType, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, UserIdBuf, }; pub use { diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index b52ddb28..5157fbc0 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -594,11 +594,11 @@ async fn test_playlist_change_detail() { #[maybe_async_test] #[ignore] async fn test_playlist_check_follow() { - let playlist_id = "2v3iNvBX8Ay1Gt2uXtUKUT"; - let mut user_ids: Vec = vec![]; - let user_id1 = String::from("possan"); + let playlist_id = Id::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); + let mut user_ids: Vec<_> = vec![]; + let user_id1 = Id::from_id("possan").unwrap(); user_ids.push(user_id1); - let user_id2 = String::from("elogain"); + let user_id2 = Id::from_id("elogain").unwrap(); user_ids.push(user_id2); oauth_client() .await @@ -611,7 +611,7 @@ async fn test_playlist_check_follow() { #[maybe_async_test] #[ignore] async fn test_user_playlist_create() { - let user_id = "2257tjys2e2u2ygfke42niy2q"; + let user_id = Id::from_id("2257tjys2e2u2ygfke42niy2q").unwrap(); let playlist_name = "A New Playlist"; oauth_client() .await @@ -624,7 +624,7 @@ async fn test_user_playlist_create() { #[maybe_async_test] #[ignore] async fn test_playlist_follow_playlist() { - let playlist_id = "2v3iNvBX8Ay1Gt2uXtUKUT"; + let playlist_id = Id::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); oauth_client() .await .playlist_follow(playlist_id, true) @@ -707,7 +707,7 @@ async fn test_playlist_replace_tracks() { #[maybe_async_test] #[ignore] async fn test_user_playlist() { - let user_id = "spotify"; + let user_id = Id::from_id("spotify").unwrap(); let playlist_id = Id::from_id("59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await @@ -720,7 +720,7 @@ async fn test_user_playlist() { #[maybe_async_test] #[ignore] async fn test_user_playlists() { - let user_id = "2257tjys2e2u2ygfke42niy2q"; + let user_id = Id::from_id("2257tjys2e2u2ygfke42niy2q").unwrap(); oauth_client() .await .user_playlists(user_id, Some(10), None) From d1322804db0c871d53dae69a8e46e6fc2611e186 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 15:17:36 +0300 Subject: [PATCH 36/59] lifetime fix --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 2d65f516..8ff0b473 100644 --- a/src/client.rs +++ b/src/client.rs @@ -842,7 +842,7 @@ impl Spotify { pub async fn playlist_check_follow<'a>( &self, playlist_id: PlaylistId<'_>, - user_ids: &[UserId<'_>], + user_ids: &'a [UserId<'a>], ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); From e5481a8ee75c0249c2ade4e1b8c674c224addf00 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 15:38:44 +0300 Subject: [PATCH 37/59] make start_playback type-safe --- src/client.rs | 18 +++++++++++------- src/model/idtypes.rs | 5 +++++ tests/test_with_oauth.rs | 12 +++++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8ff0b473..cb2fedb2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,7 +15,7 @@ use super::http::{BaseClient, Query}; use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; -use crate::model::idtypes::IdType; +use crate::model::idtypes::{IdType, PlayContextIdType}; /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] @@ -1634,23 +1634,27 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/start-a-users-playback/) #[maybe_async] - pub async fn start_playback( + pub async fn start_playback( &self, device_id: Option, - context_uri: Option, - uris: Option>, + context_uri: Option>, + uris: Option<&[Id<'_, T>]>, offset: Option, position_ms: Option, ) -> ClientResult<()> { if context_uri.is_some() && uris.is_some() { - error!("specify either contexxt uri or uris, not both"); + error!("specify either context uri or uris, not both"); } let mut params = json!({}); if let Some(context_uri) = context_uri { - json_insert!(params, "context_uri", context_uri); + json_insert!(params, "context_uri", context_uri.uri()); } if let Some(uris) = uris { - json_insert!(params, "uris", uris); + json_insert!( + params, + "uris", + uris.iter().map(|id| id.uri()).collect::>() + ); } if let Some(offset) = offset { if let Some(position) = offset.position { diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 6d7be522..d1311ff4 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -11,13 +11,16 @@ pub trait IdType: private::Sealed { const TYPE: Type; } pub trait PlayableIdType: IdType {} +pub trait PlayContextIdType: IdType {} impl IdType for Artist { const TYPE: Type = Type::Artist; } +impl PlayContextIdType for Artist {} impl IdType for Album { const TYPE: Type = Type::Album; } +impl PlayContextIdType for Album {} impl IdType for Track { const TYPE: Type = Type::Track; } @@ -25,12 +28,14 @@ impl PlayableIdType for Track {} impl IdType for Playlist { const TYPE: Type = Type::Playlist; } +impl PlayContextIdType for Playlist {} impl IdType for User { const TYPE: Type = Type::User; } impl IdType for Show { const TYPE: Type = Type::Show; } +impl PlayContextIdType for Show {} impl IdType for Episode { const TYPE: Type = Type::Episode; } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 5157fbc0..6a7177f5 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -20,7 +20,7 @@ use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; use rspotify::model::{ - Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, + idtypes, Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, }; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; @@ -484,10 +484,16 @@ async fn test_shuffle() { #[ignore] async fn test_start_playback() { let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); - let uris = vec!["spotify:track:4iV5W9uYEdYUVa79Axb7Rh".to_owned()]; + let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await - .start_playback(Some(device_id), None, Some(uris), for_position(0), None) + .start_playback::<_, idtypes::Playlist>( + Some(device_id), + None, + Some(&*uris), + for_position(0), + None, + ) .await .unwrap(); } From 728128756ddd8fe523bc12c5b2bf5ef44f896438 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 29 Dec 2020 15:44:45 +0300 Subject: [PATCH 38/59] split start_playback method into two --- src/client.rs | 47 +++++++++++++++++++++++++++------------- tests/test_with_oauth.rs | 10 ++------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/client.rs b/src/client.rs index cb2fedb2..2b5b8ecf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1634,28 +1634,45 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/start-a-users-playback/) #[maybe_async] - pub async fn start_playback( + pub async fn start_context_playback( &self, + context_uri: Id<'_, T>, device_id: Option, - context_uri: Option>, - uris: Option<&[Id<'_, T>]>, offset: Option, position_ms: Option, ) -> ClientResult<()> { - if context_uri.is_some() && uris.is_some() { - error!("specify either context uri or uris, not both"); - } let mut params = json!({}); - if let Some(context_uri) = context_uri { - json_insert!(params, "context_uri", context_uri.uri()); - } - if let Some(uris) = uris { - json_insert!( - params, - "uris", - uris.iter().map(|id| id.uri()).collect::>() - ); + json_insert!(params, "context_uri", context_uri.uri()); + if let Some(offset) = offset { + if let Some(position) = offset.position { + json_insert!(params, "offset", json!({ "position": position })); + } else if let Some(uri) = offset.uri { + json_insert!(params, "offset", json!({ "uri": uri })); + } } + if let Some(position_ms) = position_ms { + json_insert!(params, "position_ms", position_ms); + }; + let url = self.append_device_id("me/player/play", device_id); + self.put(&url, None, ¶ms).await?; + + Ok(()) + } + + #[maybe_async] + pub async fn start_uris_playback( + &self, + uris: &[Id<'_, T>], + device_id: Option, + offset: Option, + position_ms: Option, + ) -> ClientResult<()> { + let mut params = json!({}); + json_insert!( + params, + "uris", + uris.iter().map(|id| id.uri()).collect::>() + ); if let Some(offset) = offset { if let Some(position) = offset.position { json_insert!(params, "offset", json!({ "position": position })); diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 6a7177f5..5204440e 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -20,7 +20,7 @@ use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; use rspotify::model::offset::for_position; use rspotify::model::{ - idtypes, Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, + Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, }; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; @@ -487,13 +487,7 @@ async fn test_start_playback() { let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await - .start_playback::<_, idtypes::Playlist>( - Some(device_id), - None, - Some(&*uris), - for_position(0), - None, - ) + .start_uris_playback(&uris, Some(device_id), for_position(0), None) .await .unwrap(); } From 603ecc94d2f2800a5399d970633bd8e0aed339de Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 12 Jan 2021 17:09:05 +0300 Subject: [PATCH 39/59] implement serialize/deserialize for id types --- src/model/idtypes.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index d1311ff4..c1ee1bf0 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -1,8 +1,12 @@ use crate::model::Type; -use serde::export::PhantomData; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; use strum::Display; use thiserror::Error; +// This is a sealed trait pattern implementation, it stops external code from implementing the `IdType` trait. +// The `Sealed` trait must be in a private mod, so external code can not see and implement it. +// See also: https://rust-lang.github.io/api-guidelines/future-proofing.html mod private { pub trait Sealed {} } @@ -83,9 +87,11 @@ impl private::Sealed for Episode {} /// /// This is a not-owning type, it stores a &str only. /// See [IdBuf](crate::model::idtypes::IdBuf) for owned version of the type. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Id<'id, T> { + #[serde(default)] _type: PhantomData, + #[serde(flatten)] id: &'id str, } @@ -96,9 +102,11 @@ pub struct Id<'id, T> { /// /// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or `Id::from_id_or_uri(val).to_owned()` /// to construct an instance of this type. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct IdBuf { + #[serde(default)] _type: PhantomData, + #[serde(flatten)] id: String, } From 0fe650f1609c9093d6aa0ded5a2add70f84cd85e Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 12 Jan 2021 17:09:29 +0300 Subject: [PATCH 40/59] simplify offset type and make it enum --- src/client.rs | 44 ++++++++++++++++++++++++++++------------ src/model/offset.rs | 37 ++++++++++++++------------------- tests/test_models.rs | 21 ------------------- tests/test_with_oauth.rs | 9 ++++++-- 4 files changed, 53 insertions(+), 58 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2b5b8ecf..48734e09 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1634,20 +1634,29 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/start-a-users-playback/) #[maybe_async] - pub async fn start_context_playback( + pub async fn start_context_playback( &self, context_uri: Id<'_, T>, device_id: Option, - offset: Option, - position_ms: Option, + offset: Option>, + position_ms: Option, ) -> ClientResult<()> { + use super::model::Offset; + let mut params = json!({}); json_insert!(params, "context_uri", context_uri.uri()); if let Some(offset) = offset { - if let Some(position) = offset.position { - json_insert!(params, "offset", json!({ "position": position })); - } else if let Some(uri) = offset.uri { - json_insert!(params, "offset", json!({ "uri": uri })); + match offset { + Offset::Position(position) => { + json_insert!( + params, + "offset", + json!({ "position": position.as_millis() }) + ); + } + Offset::Uri(uri) => { + json_insert!(params, "offset", json!({ "uri": uri.uri() })); + } } } if let Some(position_ms) = position_ms { @@ -1660,13 +1669,15 @@ impl Spotify { } #[maybe_async] - pub async fn start_uris_playback( + pub async fn start_uris_playback( &self, uris: &[Id<'_, T>], device_id: Option, - offset: Option, + offset: Option>, position_ms: Option, ) -> ClientResult<()> { + use super::model::Offset; + let mut params = json!({}); json_insert!( params, @@ -1674,10 +1685,17 @@ impl Spotify { uris.iter().map(|id| id.uri()).collect::>() ); if let Some(offset) = offset { - if let Some(position) = offset.position { - json_insert!(params, "offset", json!({ "position": position })); - } else if let Some(uri) = offset.uri { - json_insert!(params, "offset", json!({ "uri": uri })); + match offset { + Offset::Position(position) => { + json_insert!( + params, + "offset", + json!({ "position": position.as_millis() }) + ); + } + Offset::Uri(uri) => { + json_insert!(params, "offset", json!({ "uri": uri.uri() })); + } } } if let Some(position_ms) = position_ms { diff --git a/src/model/offset.rs b/src/model/offset.rs index e102deee..1daeee8d 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -1,32 +1,25 @@ //! Offset object -use crate::model::{from_option_duration_ms, to_option_duration_ms}; -use serde::{Deserialize, Serialize}; +use crate::model::{idtypes, Id, IdBuf, PlayableIdType}; use std::time::Duration; /// Offset object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/start-a-users-playback/) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct Offset { - #[serde(default)] - #[serde( - deserialize_with = "from_option_duration_ms", - serialize_with = "to_option_duration_ms" - )] - pub position: Option, - pub uri: Option, +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Offset { + Position(Duration), + Uri(IdBuf), } -pub fn for_position(position: u64) -> Option { - Some(Offset { - position: Some(Duration::from_millis(position)), - uri: None, - }) -} +impl Offset { + pub fn for_position(position: u64) -> Offset { + Offset::Position(Duration::from_millis(position)) + } -pub fn for_uri(uri: String) -> Option { - Some(Offset { - position: None, - uri: Some(uri), - }) + pub fn for_uri(uri: Id<'_, T>) -> Offset + where + T: PlayableIdType, + { + Offset::Uri(uri.to_owned()) + } } diff --git a/tests/test_models.rs b/tests/test_models.rs index 5353d750..aea5358f 100644 --- a/tests/test_models.rs +++ b/tests/test_models.rs @@ -814,24 +814,3 @@ fn test_current_playback_context() { assert_eq!(current_playback_context.timestamp, dt); assert!(current_playback_context.progress.is_none()); } - -#[test] -fn test_offset() { - let json = r#" - { - "position": 5, - "uri": "spotify:track:1301WleyT98MSxVHPZCA6M" - } - "#; - let offset: Offset = serde_json::from_str(&json).unwrap(); - let duration = Duration::from_millis(5); - assert_eq!(offset.position, Some(duration)); - - let empty_json = r#" - { - - } - "#; - let empty_offset: Offset = serde_json::from_str(&empty_json).unwrap(); - assert!(empty_offset.position.is_none()); -} diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 5204440e..90bde721 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -18,7 +18,7 @@ mod common; use common::maybe_async_test; use rspotify::client::{Spotify, SpotifyBuilder}; -use rspotify::model::offset::for_position; +use rspotify::model::offset::Offset; use rspotify::model::{ Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, }; @@ -487,7 +487,12 @@ async fn test_start_playback() { let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await - .start_uris_playback(&uris, Some(device_id), for_position(0), None) + .start_uris_playback( + &uris, + Some(device_id), + Some(Offset::<()>::for_position(0)), + None, + ) .await .unwrap(); } From 8587edfb88729d8447be7e906c94facea7ce2084 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 12 Jan 2021 17:13:25 +0300 Subject: [PATCH 41/59] simplify join_ids --- src/client.rs | 11 ++--------- src/model/idtypes.rs | 7 +++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 48734e09..a5e54e42 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2051,16 +2051,9 @@ impl Spotify { } } +#[inline] fn join_ids<'a, T: IdType>(ids: impl IntoIterator>) -> String { - let mut out = String::new(); - let mut iter = ids.into_iter(); - if let Some(first) = iter.next() { - out += first.id(); - for id in iter { - out = out + "," + id.id(); - } - } - out + ids.into_iter().collect::>().join(",") } #[cfg(test)] diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index c1ee1bf0..713e551a 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -1,5 +1,6 @@ use crate::model::Type; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::marker::PhantomData; use strum::Display; use thiserror::Error; @@ -183,6 +184,12 @@ impl Into> for &Id<'_, T> { } } +impl Borrow for Id<'_, T> { + fn borrow(&self) -> &str { + self.id + } +} + impl std::str::FromStr for IdBuf { type Err = IdError; From 4efbe0cb63ed2d729c55245f4c33bef14dd87784 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Wed, 13 Jan 2021 07:54:55 +0300 Subject: [PATCH 42/59] make id types unsized --- src/client.rs | 116 +++++++++++++++++++++---------------------- src/model/idtypes.rs | 86 +++++++++++++++----------------- src/model/offset.rs | 2 +- src/model/track.rs | 4 +- 4 files changed, 98 insertions(+), 110 deletions(-) diff --git a/src/client.rs b/src/client.rs index a5e54e42..bec1c96d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -162,7 +162,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-track/) #[maybe_async] - pub async fn track(&self, track_id: TrackId<'_>) -> ClientResult { + pub async fn track(&self, track_id: &TrackId) -> ClientResult { let url = format!("tracks/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -178,7 +178,7 @@ impl Spotify { #[maybe_async] pub async fn tracks<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator, market: Option, ) -> ClientResult> { let ids = join_ids(track_ids); @@ -200,7 +200,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-artist/) #[maybe_async] - pub async fn artist(&self, artist_id: ArtistId<'_>) -> ClientResult { + pub async fn artist(&self, artist_id: &ArtistId) -> ClientResult { let url = format!("artists/{}", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -215,7 +215,7 @@ impl Spotify { #[maybe_async] pub async fn artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator, ) -> ClientResult> { let ids = join_ids(artist_ids); let url = format!("artists/?ids={}", ids); @@ -238,7 +238,7 @@ impl Spotify { #[maybe_async] pub async fn artist_albums( &self, - artist_id: ArtistId<'_>, + artist_id: &ArtistId, album_type: Option, country: Option, limit: Option, @@ -273,7 +273,7 @@ impl Spotify { #[maybe_async] pub async fn artist_top_tracks>>( &self, - artist_id: ArtistId<'_>, + artist_id: &ArtistId, country: T, ) -> ClientResult> { let mut params = Query::with_capacity(1); @@ -298,7 +298,7 @@ impl Spotify { #[maybe_async] pub async fn artist_related_artists( &self, - artist_id: ArtistId<'_>, + artist_id: &ArtistId, ) -> ClientResult> { let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.get(&url, None, &Query::new()).await?; @@ -313,7 +313,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-album/) #[maybe_async] - pub async fn album(&self, album_id: AlbumId<'_>) -> ClientResult { + pub async fn album(&self, album_id: &AlbumId) -> ClientResult { let url = format!("albums/{}", album_id.id()); let result = self.get(&url, None, &Query::new()).await?; @@ -329,7 +329,7 @@ impl Spotify { #[maybe_async] pub async fn albums<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator, ) -> ClientResult> { let ids = join_ids(album_ids); let url = format!("albums/?ids={}", ids); @@ -389,7 +389,7 @@ impl Spotify { #[maybe_async] pub async fn album_track>, O: Into>>( &self, - album_id: AlbumId<'_>, + album_id: &AlbumId, limit: L, offset: O, ) -> ClientResult> { @@ -408,7 +408,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-users-profile/) #[maybe_async] - pub async fn user(&self, user_id: UserId<'_>) -> ClientResult { + pub async fn user(&self, user_id: &UserId) -> ClientResult { let url = format!("users/{}", user_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -424,7 +424,7 @@ impl Spotify { #[maybe_async] pub async fn playlist( &self, - playlist_id: PlaylistId<'_>, + playlist_id: &PlaylistId, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -473,7 +473,7 @@ impl Spotify { #[maybe_async] pub async fn user_playlists>, O: Into>>( &self, - user_id: UserId<'_>, + user_id: &UserId, limit: L, offset: O, ) -> ClientResult> { @@ -496,8 +496,8 @@ impl Spotify { #[maybe_async] pub async fn user_playlist( &self, - user_id: UserId<'_>, - playlist_id: Option>, + user_id: &UserId, + playlist_id: Option<&PlaylistId>, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -535,7 +535,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_tracks>, O: Into>>( &self, - playlist_id: PlaylistId<'_>, + playlist_id: &PlaylistId, fields: Option<&str>, limit: L, offset: O, @@ -567,7 +567,7 @@ impl Spotify { #[maybe_async] pub async fn user_playlist_create>, D: Into>>( &self, - user_id: UserId<'_>, + user_id: &UserId, name: &str, public: P, description: D, @@ -643,8 +643,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_add_tracks<'a>( &self, - playlist_id: PlaylistId<'_>, - track_ids: impl IntoIterator>, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, position: Option, ) -> ClientResult { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -669,8 +669,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_replace_tracks<'a>( &self, - playlist_id: PlaylistId<'_>, - track_ids: impl IntoIterator>, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, ) -> ClientResult<()> { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -694,7 +694,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_reorder_tracks>>( &self, - playlist_id: PlaylistId<'_>, + playlist_id: &PlaylistId, range_start: i32, range_length: R, insert_before: i32, @@ -725,8 +725,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, - playlist_id: PlaylistId<'_>, - track_ids: impl IntoIterator>, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, snapshot_id: Option, ) -> ClientResult { let tracks = track_ids @@ -781,7 +781,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_specific_occurrences_of_tracks( &self, - playlist_id: PlaylistId<'_>, + playlist_id: &PlaylistId, tracks: Vec>, snapshot_id: Option, ) -> ClientResult { @@ -813,7 +813,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_follow>>( &self, - playlist_id: PlaylistId<'_>, + playlist_id: &PlaylistId, public: P, ) -> ClientResult<()> { let url = format!("playlists/{}/followers", playlist_id.id()); @@ -841,8 +841,8 @@ impl Spotify { #[maybe_async] pub async fn playlist_check_follow<'a>( &self, - playlist_id: PlaylistId<'_>, - user_ids: &'a [UserId<'a>], + playlist_id: &PlaylistId, + user_ids: &'a [&'a UserId], ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); @@ -975,7 +975,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.delete(&url, None, &json!({})).await?; @@ -993,7 +993,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator, ) -> ClientResult> { let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.get(&url, None, &Query::new()).await?; @@ -1009,7 +1009,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.put(&url, None, &json!({})).await?; @@ -1109,7 +1109,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.put(&url, None, &json!({})).await?; @@ -1126,7 +1126,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.delete(&url, None, &json!({})).await?; @@ -1144,7 +1144,7 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator>, + album_ids: impl IntoIterator, ) -> ClientResult> { let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.get(&url, None, &Query::new()).await?; @@ -1160,7 +1160,7 @@ impl Spotify { #[maybe_async] pub async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.put(&url, None, &json!({})).await?; @@ -1177,7 +1177,7 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.delete(&url, None, &json!({})).await?; @@ -1195,7 +1195,7 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artist_ids: impl IntoIterator>, + artist_ids: impl IntoIterator, ) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", @@ -1214,7 +1214,7 @@ impl Spotify { #[maybe_async] pub async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.put(&url, None, &json!({})).await?; @@ -1231,7 +1231,7 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator>, + user_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.delete(&url, None, &json!({})).await?; @@ -1394,9 +1394,9 @@ impl Spotify { #[maybe_async] pub async fn recommendations>>( &self, - seed_artists: Option>>, + seed_artists: Option>, seed_genres: Option>, - seed_tracks: Option>>, + seed_tracks: Option>, limit: L, country: Option, payload: &Map, @@ -1459,7 +1459,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-features/) #[maybe_async] - pub async fn track_features(&self, track_id: TrackId<'_>) -> ClientResult { + pub async fn track_features(&self, track_id: &TrackId) -> ClientResult { let url = format!("audio-features/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1474,7 +1474,7 @@ impl Spotify { #[maybe_async] pub async fn tracks_features<'a>( &self, - track_ids: impl IntoIterator>, + track_ids: impl IntoIterator, ) -> ClientResult>> { let url = format!("audio-features/?ids={}", join_ids(track_ids)); @@ -1494,7 +1494,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/web-api/get-audio-analysis/) #[maybe_async] - pub async fn track_analysis(&self, track_id: TrackId<'_>) -> ClientResult { + pub async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { let url = format!("audio-analysis/{}", track_id.id()); let result = self.get(&url, None, &Query::new()).await?; self.convert_result(&result) @@ -1636,7 +1636,7 @@ impl Spotify { #[maybe_async] pub async fn start_context_playback( &self, - context_uri: Id<'_, T>, + context_uri: &Id, device_id: Option, offset: Option>, position_ms: Option, @@ -1671,7 +1671,7 @@ impl Spotify { #[maybe_async] pub async fn start_uris_playback( &self, - uris: &[Id<'_, T>], + uris: &[&Id], device_id: Option, offset: Option>, position_ms: Option, @@ -1836,7 +1836,7 @@ impl Spotify { #[maybe_async] pub async fn add_item_to_queue( &self, - item: Id<'_, T>, + item: &Id, device_id: Option, ) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); @@ -1854,7 +1854,7 @@ impl Spotify { #[maybe_async] pub async fn save_shows<'a>( &self, - show_ids: impl IntoIterator>, + show_ids: impl IntoIterator, ) -> ClientResult<()> { let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.put(&url, None, &json!({})).await?; @@ -1893,11 +1893,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/shows/get-a-show/) #[maybe_async] - pub async fn get_a_show( - &self, - id: ShowId<'_>, - market: Option, - ) -> ClientResult { + pub async fn get_a_show(&self, id: &ShowId, market: Option) -> ClientResult { let mut params = Query::new(); if let Some(market) = market { params.insert("country".to_owned(), market.to_string()); @@ -1918,7 +1914,7 @@ impl Spotify { #[maybe_async] pub async fn get_several_shows<'a>( &self, - ids: impl IntoIterator>, + ids: impl IntoIterator, market: Option, ) -> ClientResult> { let mut params = Query::with_capacity(1); @@ -1946,7 +1942,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: ShowId<'_>, + id: &ShowId, limit: L, offset: O, market: Option, @@ -1974,7 +1970,7 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: EpisodeId<'_>, + id: &EpisodeId, market: Option, ) -> ClientResult { let url = format!("episodes/{}", id.id()); @@ -1997,7 +1993,7 @@ impl Spotify { #[maybe_async] pub async fn get_several_episodes<'a>( &self, - ids: impl IntoIterator>, + ids: impl IntoIterator, market: Option, ) -> ClientResult { let mut params = Query::with_capacity(1); @@ -2018,7 +2014,7 @@ impl Spotify { #[maybe_async] pub async fn check_users_saved_shows<'a>( &self, - ids: impl IntoIterator>, + ids: impl IntoIterator, ) -> ClientResult> { let mut params = Query::with_capacity(1); params.insert("ids".to_owned(), join_ids(ids)); @@ -2037,7 +2033,7 @@ impl Spotify { #[maybe_async] pub async fn remove_users_saved_shows<'a>( &self, - show_ids: impl IntoIterator>, + show_ids: impl IntoIterator, market: Option, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); @@ -2052,7 +2048,7 @@ impl Spotify { } #[inline] -fn join_ids<'a, T: IdType>(ids: impl IntoIterator>) -> String { +fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { ids.into_iter().collect::>().join(",") } diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 713e551a..ab5112e8 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -46,13 +46,13 @@ impl IdType for Episode { } impl PlayableIdType for Episode {} -pub type ArtistId<'a> = Id<'a, Artist>; -pub type AlbumId<'a> = Id<'a, Album>; -pub type TrackId<'a> = Id<'a, Track>; -pub type PlaylistId<'a> = Id<'a, Playlist>; -pub type UserId<'a> = Id<'a, User>; -pub type ShowId<'a> = Id<'a, Show>; -pub type EpisodeId<'a> = Id<'a, Episode>; +pub type ArtistId = Id; +pub type AlbumId = Id; +pub type TrackId = Id; +pub type PlaylistId = Id; +pub type UserId = Id; +pub type ShowId = Id; +pub type EpisodeId = Id; pub type ArtistIdBuf = IdBuf; pub type AlbumIdBuf = IdBuf; @@ -88,12 +88,12 @@ impl private::Sealed for Episode {} /// /// This is a not-owning type, it stores a &str only. /// See [IdBuf](crate::model::idtypes::IdBuf) for owned version of the type. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct Id<'id, T> { +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct Id { #[serde(default)] _type: PhantomData, #[serde(flatten)] - id: &'id str, + id: str, } /// A Spotify object id of given [type](crate::model::enums::types::Type) @@ -111,21 +111,21 @@ pub struct IdBuf { id: String, } -impl<'id, T> Into> for &'id IdBuf { - fn into(self) -> Id<'id, T> { - Id { - _type: PhantomData, - id: &self.id, - } +impl AsRef> for IdBuf { + fn as_ref(&self) -> &Id { + // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag + unsafe { std::mem::transmute(&*self.id) } } } -impl IdBuf { - /// Get a non-owning [`Id`](crate::model::idtypes::Id) representation of the id - pub fn as_ref(&self) -> Id<'_, T> { - self.into() +impl Borrow> for IdBuf { + fn borrow(&self) -> &Id { + // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag + unsafe { std::mem::transmute(&*self.id) } } +} +impl IdBuf { /// Get a [`Type`](crate::model::enums::types::Type) of the id pub fn _type(&self) -> Type { T::TYPE @@ -163,30 +163,21 @@ pub enum IdError { InvalidId, } -impl std::fmt::Display for Id<'_, T> { +impl std::fmt::Display for &Id { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "spotify:{}:{}", T::TYPE, self.id) + write!(f, "spotify:{}:{}", T::TYPE, &self.id) } } -impl AsRef for Id<'_, T> { +impl AsRef for &Id { fn as_ref(&self) -> &str { - self.id - } -} - -impl Into> for &Id<'_, T> { - fn into(self) -> IdBuf { - IdBuf { - _type: PhantomData, - id: self.id.to_owned(), - } + &self.id } } -impl Borrow for Id<'_, T> { +impl Borrow for &Id { fn borrow(&self) -> &str { - self.id + &self.id } } @@ -198,10 +189,13 @@ impl std::str::FromStr for IdBuf { } } -impl Id<'_, T> { +impl Id { /// Owned version of the id [`IdBuf`](crate::model::idtypes::IdBuf) pub fn to_owned(&self) -> IdBuf { - self.into() + IdBuf { + _type: PhantomData, + id: (&self.id).to_owned(), + } } /// Spotify object type @@ -211,21 +205,21 @@ impl Id<'_, T> { /// Spotify object id (guaranteed to be a string of alphanumeric characters) pub fn id(&self) -> &str { - self.id + &self.id } /// Spotify object URI in a well-known format: spotify:type:id /// /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. pub fn uri(&self) -> String { - format!("spotify:{}:{}", T::TYPE, self.id) + format!("spotify:{}:{}", T::TYPE, &self.id) } /// Full Spotify object URL, can be opened in a browser /// /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI pub fn url(&self) -> String { - format!("https://open.spotify.com/{}/{}", T::TYPE, self.id) + format!("https://open.spotify.com/{}/{}", T::TYPE, &self.id) } /// Parse Spotify id or URI from string slice @@ -246,7 +240,7 @@ impl Id<'_, 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 non-alphanumeric characters), /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. - pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result, IdError> { + pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result<&'a Id, IdError> { match Id::::from_uri(id_or_uri) { Ok(id) => Ok(id), Err(IdError::InvalidPrefix) => Id::::from_id(id_or_uri), @@ -261,12 +255,10 @@ impl Id<'_, T> { /// # Errors: /// /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. - pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result, IdError> { + pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result<&'a Id, IdError> { if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { - Ok(Id { - _type: PhantomData, - id, - }) + // Safe, b/c Id is just a str with ZST type tag, and id is proved to be a valid id at this point + Ok(unsafe { std::mem::transmute(id) }) } else { Err(IdError::InvalidId) } @@ -286,7 +278,7 @@ impl Id<'_, T> { /// - `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. - pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result, IdError> { + pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result<&'a Id, IdError> { let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; let sep = match rest.chars().next() { Some(ch) if ch == '/' || ch == ':' => ch, diff --git a/src/model/offset.rs b/src/model/offset.rs index 1daeee8d..c4c28576 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -16,7 +16,7 @@ impl Offset { Offset::Position(Duration::from_millis(position)) } - pub fn for_uri(uri: Id<'_, T>) -> Offset + pub fn for_uri(uri: &Id) -> Offset where T: PlayableIdType, { diff --git a/src/model/track.rs b/src/model/track.rs index 9b27cb2f..349d65ce 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -112,13 +112,13 @@ pub struct SavedTrack { /// Track id with specific positions track in a playlist pub struct TrackPositions<'id> { - pub id: TrackId<'id>, + pub id: &'id TrackId, pub positions: Vec, } impl<'id> TrackPositions<'id> { /// Track in a playlist by an id - pub fn new(id: TrackId<'id>, positions: Vec) -> Self { + pub fn new(id: &'id TrackId, positions: Vec) -> Self { Self { id, positions } } } From 8744b2000b6db78305173dad20cb6576f1b05f2a Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Wed, 13 Jan 2021 09:21:52 +0300 Subject: [PATCH 43/59] replace transmute with typecast --- src/model/idtypes.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index ab5112e8..726f49c3 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -114,14 +114,14 @@ pub struct IdBuf { impl AsRef> for IdBuf { fn as_ref(&self) -> &Id { // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag - unsafe { std::mem::transmute(&*self.id) } + unsafe { &*(&*self.id as *const str as *const Id) } } } impl Borrow> for IdBuf { fn borrow(&self) -> &Id { // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag - unsafe { std::mem::transmute(&*self.id) } + unsafe { &*(&*self.id as *const str as *const Id) } } } @@ -258,7 +258,7 @@ impl Id { pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result<&'a Id, IdError> { if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { // Safe, b/c Id is just a str with ZST type tag, and id is proved to be a valid id at this point - Ok(unsafe { std::mem::transmute(id) }) + Ok(unsafe { &*(id as *const str as *const Id) }) } else { Err(IdError::InvalidId) } From 51750c438d3d1fdabe88ba069f30603106c113fa Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 26 Feb 2021 16:09:28 +0300 Subject: [PATCH 44/59] use macro to reduce boilerplate --- src/model/idtypes.rs | 56 ++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 726f49c3..4ba5ab12 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -18,32 +18,27 @@ pub trait IdType: private::Sealed { pub trait PlayableIdType: IdType {} pub trait PlayContextIdType: IdType {} -impl IdType for Artist { - const TYPE: Type = Type::Artist; + +macro_rules! sealed_types { + ($($name:ident),+) => { + $( + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum $name {} + impl private::Sealed for $name {} + impl IdType for $name { + const TYPE: Type = Type::$name; + } + )+ + } } + +sealed_types!(Artist, Album, Track, Playlist, User, Show, Episode); + impl PlayContextIdType for Artist {} -impl IdType for Album { - const TYPE: Type = Type::Album; -} impl PlayContextIdType for Album {} -impl IdType for Track { - const TYPE: Type = Type::Track; -} impl PlayableIdType for Track {} -impl IdType for Playlist { - const TYPE: Type = Type::Playlist; -} impl PlayContextIdType for Playlist {} -impl IdType for User { - const TYPE: Type = Type::User; -} -impl IdType for Show { - const TYPE: Type = Type::Show; -} impl PlayContextIdType for Show {} -impl IdType for Episode { - const TYPE: Type = Type::Episode; -} impl PlayableIdType for Episode {} pub type ArtistId = Id; @@ -62,27 +57,6 @@ pub type UserIdBuf = IdBuf; pub type ShowIdBuf = IdBuf; pub type EpisodeIdBuf = IdBuf; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Artist {} -impl private::Sealed for Artist {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Album {} -impl private::Sealed for Album {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Track {} -impl private::Sealed for Track {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Playlist {} -impl private::Sealed for Playlist {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum User {} -impl private::Sealed for User {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Show {} -impl private::Sealed for Show {} -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Episode {} -impl private::Sealed for Episode {} /// A Spotify object id of given [type](crate::model::enums::types::Type) /// From 5069e082c9df9e3244da4033d8afbb1e572ff2aa Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 26 Feb 2021 16:10:48 +0300 Subject: [PATCH 45/59] fmt --- src/model/idtypes.rs | 2 -- tests/test_with_credential.rs | 2 +- tests/test_with_oauth.rs | 4 +++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 4ba5ab12..82fba513 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -18,7 +18,6 @@ pub trait IdType: private::Sealed { pub trait PlayableIdType: IdType {} pub trait PlayContextIdType: IdType {} - macro_rules! sealed_types { ($($name:ident),+) => { $( @@ -57,7 +56,6 @@ pub type UserIdBuf = IdBuf; pub type ShowIdBuf = IdBuf; pub type EpisodeIdBuf = IdBuf; - /// A Spotify object id of given [type](crate::model::enums::types::Type) /// /// This is a not-owning type, it stores a &str only. diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 04a63cf8..a85f49b3 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -4,7 +4,7 @@ use common::maybe_async_test; use rspotify::oauth2::CredentialsBuilder; use rspotify::{ client::{Spotify, SpotifyBuilder}, - model::{Market, AlbumType, Country, Id}, + model::{AlbumType, Country, Id, Market}, }; use maybe_async::maybe_async; diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 7a09a6f1..7956d104 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -21,7 +21,9 @@ use rspotify::model::offset::Offset; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use rspotify::{ client::{Spotify, SpotifyBuilder}, - model::{Market, Country, Id, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions}, + model::{ + Country, Id, Market, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, + }, }; use chrono::prelude::*; From 19b3a3dcc07b5a8ac26b284b8e1c1fea76f46564 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 26 Feb 2021 16:20:02 +0300 Subject: [PATCH 46/59] reformat comments --- src/model/idtypes.rs | 71 +++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 82fba513..870e2029 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -5,8 +5,9 @@ use std::marker::PhantomData; use strum::Display; use thiserror::Error; -// This is a sealed trait pattern implementation, it stops external code from implementing the `IdType` trait. -// The `Sealed` trait must be in a private mod, so external code can not see and implement it. +// This is a sealed trait pattern implementation, it stops external code from +// implementing the `IdType` trait. The `Sealed` trait must be in a private mod, +// so external code can not see and implement it. // See also: https://rust-lang.github.io/api-guidelines/future-proofing.html mod private { pub trait Sealed {} @@ -73,8 +74,8 @@ pub struct Id { /// This is an owning type, it stores a String. /// See [IdBuf](crate::model::idtypes::Id) for light-weight non-owning type. /// -/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or `Id::from_id_or_uri(val).to_owned()` -/// to construct an instance of this type. +/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or +/// `Id::from_id_or_uri(val).to_owned()` to construct an instance of this type. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct IdBuf { #[serde(default)] @@ -126,10 +127,11 @@ impl IdBuf { pub enum IdError { /// 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) + /// 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) InvalidType, /// Spotify id is invalid (empty or contains non-alphanumeric characters) InvalidId, @@ -182,36 +184,46 @@ impl Id { /// Spotify object URI in a well-known format: spotify:type:id /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify:track:4y4VO05kYgUTo2bzbox1an`. + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify:track:4y4VO05kYgUTo2bzbox1an`. pub fn uri(&self) -> String { format!("spotify:{}:{}", T::TYPE, &self.id) } /// Full Spotify object URL, can be opened in a browser /// - /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI + /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, + /// https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI pub fn url(&self) -> String { format!("https://open.spotify.com/{}/{}", T::TYPE, &self.id) } /// 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 alphanumeric string. - /// The URI must be of given `T`ype, otherwise `IdError::InvalidType` error is returned. + /// 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 + /// alphanumeric string. + /// The URI must be of given `T`ype, otherwise `IdError::InvalidType` + /// error is returned. /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// 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 alphanumeric string. + /// 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 alphanumeric string. /// /// # Errors: /// - /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part is not equal to `_type`, - /// - `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 non-alphanumeric characters), - /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be split into type and id parts. + /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part + /// is not equal to `_type`, + /// - `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 + /// non-alphanumeric characters), + /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be + /// split into type and id parts. pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result<&'a Id, IdError> { match Id::::from_uri(id_or_uri) { Ok(id) => Ok(id), @@ -238,18 +250,23 @@ impl Id { /// 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 alphanumeric string. + /// 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 alphanumeric string. /// - /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// 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::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. + /// - `IdError::InvalidFormat` - if it can't be splitted into type and + /// id parts. pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result<&'a Id, IdError> { let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; let sep = match rest.chars().next() { From ebcb2d62a539be7c865eae2936e9cc4a3edebf83 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Fri, 26 Feb 2021 16:23:36 +0300 Subject: [PATCH 47/59] updated changelog --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37646b19..b4044196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,15 +107,15 @@ If we missed any change or there's something you'd like to discuss about this ve + The `ClientError::CLI` variant, for whenever user interaction goes wrong - Fix typo in `user_playlist_remove_specific_occurrenes_of_tracks`, now it's `user_playlist_remove_specific_occurrences_of_tracks`. - ([#123](https://github.com/ramsayleung/rspotify/pull/123))All fallible calls in the client return a `ClientError` rather than using `failure`. -- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints take `Vec>/&[Id<'_, Type>]` as parameter have changed to `impl IntoIterator>`. - + The endpoints which changes parameter from `Vec` to `impl IntoIterator>`: +- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints take `Vec<&Id>/&[&Id]` as parameter have changed to `impl IntoIterator>`. + + The endpoints which changes parameter from `Vec` to `impl IntoIterator>`: - `artists` - `albums` - `save_shows` - `get_several_episodes` - `check_users_saved_shows` - `remove_users_saved_shows` - + The endpoints which changes parameter from `&[String]` to `impl IntoIterator>`: + + The endpoints which changes parameter from `&[String]` to `impl IntoIterator>`: - `user_playlist_add_tracks` - `user_playlist_replace_tracks` - `user_playlist_remove_all_occurrences_of_tracks` @@ -131,7 +131,7 @@ If we missed any change or there's something you'd like to discuss about this ve - `user_follow_users` - `user_unfollow_users` - `audios_features` - + The endpoints which changes parameter from `String` to `Id<'_, Type>`: + + The endpoints which changes parameter from `String` to `&Id`: - `get_a_show` - `get_an_episode` - `get_shows_episodes` @@ -165,7 +165,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Change `{FullArtist, FullPlaylist, PublicUser, PrivateUser}::followers` from `HashMap>` to struct `Followers` + Replace `Actions::disallows` with a `Vec` by removing all entires whose value is false, which will result in a simpler API + Replace `{FullAlbum, SimplifiedEpisode, FullEpisode}::release_date_precision` from `String` to `DatePrecision` enum, makes it easier to use. - + Id and URI parameters are type-safe now everywhere, `Id<'_, Type>` and `IdBuf` types for ids/URIs added (non-owning and owning structs). + + Id and URI parameters are type-safe now everywhere, `Id` and `IdBuf` types for ids/URIs added (non-owning and owning structs). - ([#157](https://github.com/ramsayleung/rspotify/pull/157))Keep polishing models to make it easier to use: + Constrain visibility of `FullArtists` struct with `pub (in crate)`, make `artists` and `artist_related_artists` endpoints return a `Vec` instead. + Constrain visibility of `FullTracks` struct with `pub (in crate)`, make `tracks` and `artist_top_tracks` endpoints return a `Vec` instead. From 5d8f9d21cd4ae042a43714a85b3f04d909d3c6c6 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 11:29:23 +0300 Subject: [PATCH 48/59] reformat comments --- src/model/idtypes.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 870e2029..12221706 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -86,15 +86,15 @@ pub struct IdBuf { impl AsRef> for IdBuf { fn as_ref(&self) -> &Id { - // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag + // Safe, b/c of the same T between types, IdBuf can't be constructed + // from invalid id, and Id is just a wrapped str with ZST type tag unsafe { &*(&*self.id as *const str as *const Id) } } } impl Borrow> for IdBuf { fn borrow(&self) -> &Id { - // Safe, b/c of the same T between types, IdBuf can't be constructed from invalid id, and Id is just a wrapped str with ZST type tag - unsafe { &*(&*self.id as *const str as *const Id) } + self.as_ref() } } From 9e4adbd071d26510c0c066486692eb9bb139ad04 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 11:57:21 +0300 Subject: [PATCH 49/59] type-safer offset ctor --- src/model/offset.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/model/offset.rs b/src/model/offset.rs index c4c28576..a0147873 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -11,15 +11,14 @@ pub enum Offset { Uri(IdBuf), } -impl Offset { +impl Offset { pub fn for_position(position: u64) -> Offset { Offset::Position(Duration::from_millis(position)) } +} - pub fn for_uri(uri: &Id) -> Offset - where - T: PlayableIdType, - { +impl Offset { + pub fn for_uri(uri: &Id) -> Offset { Offset::Uri(uri.to_owned()) } } From e241820c179b419ee8398747d4fea1f5930211ee Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 12:10:05 +0300 Subject: [PATCH 50/59] typo fix, deref for idbuf --- src/model/idtypes.rs | 9 +++++++++ src/model/offset.rs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 12221706..99d90d07 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -4,6 +4,7 @@ use std::borrow::Borrow; use std::marker::PhantomData; use strum::Display; use thiserror::Error; +use std::ops::Deref; // This is a sealed trait pattern implementation, it stops external code from // implementing the `IdType` trait. The `Sealed` trait must be in a private mod, @@ -98,6 +99,14 @@ impl Borrow> for IdBuf { } } +impl Deref for IdBuf { + type Target = Id; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + impl IdBuf { /// Get a [`Type`](crate::model::enums::types::Type) of the id pub fn _type(&self) -> Type { diff --git a/src/model/offset.rs b/src/model/offset.rs index a0147873..1ce82cde 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -17,7 +17,7 @@ impl Offset { } } -impl Offset { +impl Offset { pub fn for_uri(uri: &Id) -> Offset { Offset::Uri(uri.to_owned()) } From 2403a6898bb3ab3b2010a1a64038529b3bc82950 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 12:12:41 +0300 Subject: [PATCH 51/59] more comments format --- src/model/idtypes.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 99d90d07..37be0b84 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -250,7 +250,8 @@ impl Id { /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result<&'a Id, IdError> { if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { - // Safe, b/c Id is just a str with ZST type tag, and id is proved to be a valid id at this point + // Safe, b/c Id is just a str with ZST type tag, and id is proved + // to be a valid id at this point Ok(unsafe { &*(id as *const str as *const Id) }) } else { Err(IdError::InvalidId) From 6a56b97b7cc5744bd5af2a813b37a7802ffe346f Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 12:25:32 +0300 Subject: [PATCH 52/59] fix test --- tests/test_with_oauth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 7956d104..9a1a283b 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -508,7 +508,7 @@ async fn test_start_playback() { .start_uris_playback( &uris, Some(device_id), - Some(Offset::<()>::for_position(0)), + Some(Offset::for_position(0)), None, ) .await From 1174f04f2274198af5892784240214e55a7a8e56 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 12:26:52 +0300 Subject: [PATCH 53/59] fmt --- src/model/idtypes.rs | 2 +- tests/test_with_oauth.rs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 37be0b84..b4a6788e 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -2,9 +2,9 @@ use crate::model::Type; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::marker::PhantomData; +use std::ops::Deref; use strum::Display; use thiserror::Error; -use std::ops::Deref; // This is a sealed trait pattern implementation, it stops external code from // implementing the `IdType` trait. The `Sealed` trait must be in a private mod, diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 9a1a283b..28b68ba5 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -505,12 +505,7 @@ async fn test_start_playback() { let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await - .start_uris_playback( - &uris, - Some(device_id), - Some(Offset::for_position(0)), - None, - ) + .start_uris_playback(&uris, Some(device_id), Some(Offset::for_position(0)), None) .await .unwrap(); } From 8ba5fbc91039f86bb65c5df6bab9e55508c3cec4 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 28 Feb 2021 12:33:27 +0300 Subject: [PATCH 54/59] even more type safe offset --- src/client.rs | 4 ++-- src/model/offset.rs | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 76454c65..df79cd96 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1664,11 +1664,11 @@ impl Spotify { } #[maybe_async] - pub async fn start_uris_playback( + pub async fn start_uris_playback( &self, uris: &[&Id], device_id: Option, - offset: Option>, + offset: Option>, position_ms: Option, ) -> ClientResult<()> { use super::model::Offset; diff --git a/src/model/offset.rs b/src/model/offset.rs index 1ce82cde..7993dda1 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -1,5 +1,5 @@ //! Offset object -use crate::model::{idtypes, Id, IdBuf, PlayableIdType}; +use crate::model::{Id, IdBuf, PlayableIdType}; use std::time::Duration; /// Offset object @@ -11,13 +11,10 @@ pub enum Offset { Uri(IdBuf), } -impl Offset { - pub fn for_position(position: u64) -> Offset { +impl Offset { + pub fn for_position(position: u64) -> Offset { Offset::Position(Duration::from_millis(position)) } -} - -impl Offset { pub fn for_uri(uri: &Id) -> Offset { Offset::Uri(uri.to_owned()) } From 81394ca4f36d975d57471a6b976743ad3aaed215 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Tue, 2 Mar 2021 15:04:06 +0300 Subject: [PATCH 55/59] fix offset position type --- src/client.rs | 12 ++---------- src/model/offset.rs | 7 +++---- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index df79cd96..27781d1a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1643,11 +1643,7 @@ impl Spotify { if let Some(offset) = offset { match offset { Offset::Position(position) => { - json_insert!( - params, - "offset", - json!({ "position": position.as_millis() }) - ); + json_insert!(params, "offset", json!({ "position": position })); } Offset::Uri(uri) => { json_insert!(params, "offset", json!({ "uri": uri.uri() })); @@ -1682,11 +1678,7 @@ impl Spotify { if let Some(offset) = offset { match offset { Offset::Position(position) => { - json_insert!( - params, - "offset", - json!({ "position": position.as_millis() }) - ); + json_insert!(params, "offset", json!({ "position": position })); } Offset::Uri(uri) => { json_insert!(params, "offset", json!({ "uri": uri.uri() })); diff --git a/src/model/offset.rs b/src/model/offset.rs index 7993dda1..80908062 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -1,19 +1,18 @@ //! Offset object use crate::model::{Id, IdBuf, PlayableIdType}; -use std::time::Duration; /// Offset object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/start-a-users-playback/) #[derive(Clone, Debug, PartialEq, Eq)] pub enum Offset { - Position(Duration), + Position(u32), Uri(IdBuf), } impl Offset { - pub fn for_position(position: u64) -> Offset { - Offset::Position(Duration::from_millis(position)) + pub fn for_position(position: u32) -> Offset { + Offset::Position(position) } pub fn for_uri(uri: &Id) -> Offset { Offset::Uri(uri.to_owned()) From 15e0c2326a32270455f7f4213b0fbe187a8ef8e4 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Thu, 4 Mar 2021 13:08:17 +0300 Subject: [PATCH 56/59] update docs and changelog --- CHANGELOG.md | 1 + src/model/idtypes.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4044196..34072583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,7 @@ If we missed any change or there's something you'd like to discuss about this ve - `get_shows_episodes` + The endpoint which changes parameter from `Vec>` to `Vec`: - `playlist_remove_specific_occurrences_of_tracks` +- The `Offset` type is now an enum to match API logic, `Offset::Position` is `u32` now (it's not a position in time, it's a position in a playlist, and you can't have both `position` and `uri` fields at the same time). - ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Rename endpoints with more fitting name: + `audio_analysis` -> `track_analysis` + `audio_features` -> `track_features` diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index b4a6788e..1a95c249 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -227,7 +227,7 @@ impl Id { /// # Errors: /// /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part - /// is not equal to `_type`, + /// 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 /// non-alphanumeric characters), From 0c5f70274a40074e3d4708e7047b8376aa00d9c2 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 7 Mar 2021 11:10:27 +0300 Subject: [PATCH 57/59] fix docs typos --- CHANGELOG.md | 2 +- src/model/idtypes.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34072583..f42e549f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,7 +107,7 @@ If we missed any change or there's something you'd like to discuss about this ve + The `ClientError::CLI` variant, for whenever user interaction goes wrong - Fix typo in `user_playlist_remove_specific_occurrenes_of_tracks`, now it's `user_playlist_remove_specific_occurrences_of_tracks`. - ([#123](https://github.com/ramsayleung/rspotify/pull/123))All fallible calls in the client return a `ClientError` rather than using `failure`. -- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints take `Vec<&Id>/&[&Id]` as parameter have changed to `impl IntoIterator>`. +- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints taking `Vec/&[String]` as parameter have changed to `impl IntoIterator>`. + The endpoints which changes parameter from `Vec` to `impl IntoIterator>`: - `artists` - `albums` diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 1a95c249..a934f918 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -73,7 +73,7 @@ pub struct Id { /// A Spotify object id of given [type](crate::model::enums::types::Type) /// /// This is an owning type, it stores a String. -/// See [IdBuf](crate::model::idtypes::Id) for light-weight non-owning type. +/// See [Id](crate::model::idtypes::Id) for light-weight non-owning type. /// /// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or /// `Id::from_id_or_uri(val).to_owned()` to construct an instance of this type. From 11fc5b01fcbaed76273b243a5879a757e5c6a87c Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 7 Mar 2021 11:36:00 +0300 Subject: [PATCH 58/59] remove unsafe code --- src/model/idtypes.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index a934f918..3e982b48 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -279,12 +279,10 @@ impl Id { /// id parts. pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result<&'a Id, IdError> { let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; - let sep = match rest.chars().next() { - Some(ch) if ch == '/' || ch == ':' => ch, - _ => return Err(IdError::InvalidPrefix), - }; - // It's safe to do .get_unchecked() because we checked the first char above - let rest = unsafe { rest.get_unchecked(1..) }; + let (sep, rest) = rest.split_at(1); + if sep != "/" && sep != ":" { + return Err(IdError::InvalidPrefix); + } let (tpe, id) = rest .rfind(sep) From 141e6e02544b5af5528677ac3b173efb4b9550c7 Mon Sep 17 00:00:00 2001 From: Konstantin Stepanov Date: Sun, 7 Mar 2021 11:42:08 +0300 Subject: [PATCH 59/59] better fix --- src/model/idtypes.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs index 3e982b48..c2091ca7 100644 --- a/src/model/idtypes.rs +++ b/src/model/idtypes.rs @@ -278,11 +278,15 @@ impl Id { /// - `IdError::InvalidFormat` - if it can't be splitted into type and /// id parts. pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result<&'a Id, IdError> { - let rest = uri.strip_prefix("spotify").ok_or(IdError::InvalidPrefix)?; - let (sep, rest) = rest.split_at(1); - if sep != "/" && sep != ":" { - return Err(IdError::InvalidPrefix); - } + 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)