diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc3602a..9fdce9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ If we missed any change or there's something you'd like to discuss about this ve - Rewritten documentation in hopes that it's easier to get started with Rspotify. - Reduced the number of examples. Instead of having an example for each endpoint, which is repetitive and unhelpful for newcomers, some real-life examples are now included. If you'd like to add your own example, please do! ([#113](https://github.com/ramsayleung/rspotify/pull/113)) +- Rspotify now uses macros internally to make the endpoints as concise as possible and nice to read. - Add `add_item_to_queue` endpoint. - Add `category_playlists` endpoint ([#153](https://github.com/ramsayleung/rspotify/pull/153)). - Fix race condition when using a single client from multiple threads ([#114](https://github.com/ramsayleung/rspotify/pull/114)). @@ -84,6 +85,7 @@ If we missed any change or there's something you'd like to discuss about this ve - ([#189](https://github.com/ramsayleung/rspotify/pull/189)) Add `scopes!` macro to generate scope for `Token` from string literal **Breaking changes:** +- ([#202](https://github.com/ramsayleung/rspotify/pull/202)) Rspotify now consistently uses `Option` for optional parameters. Those generic over `Into>` have been changed, which makes calling endpoints a bit ugiler but more consistent and simpler. - `SpotifyClientCredentials` has been renamed to `Credentials` ([#129](https://github.com/ramsayleung/rspotify/pull/129)), and its members `client_id` and `client_secret` to `id` and `secret`, respectively. - `TokenInfo` has been renamed to `Token`. It no longer has the `token_type` member, as it's always `Bearer` for now ([#129](https://github.com/ramsayleung/rspotify/pull/129)). - `SpotifyOAuth` has been renamed to `OAuth`. It only contains the necessary parameters for OAuth authorization instead of repeating the items from `Credentials` and `Spotify`, so `client_id`, `client_secret` and `cache_path` are no longer in `OAuth` ([#129](https://github.com/ramsayleung/rspotify/pull/129)). @@ -205,11 +207,14 @@ If we missed any change or there's something you'd like to discuss about this ve + Rename `ClientError::IO` to `ClientError::Io` + Rename `ClientError::CLI` to `ClientError::Cli` + Rename `BaseHTTPClient` to `BaseHttpClient` -- [#166](https://github.com/ramsayleung/rspotify/pull/166) [#201](https://github.com/ramsayleung/rspotify/pull/201) Add automatic pagination, which is now enabled by default. You can still use the methods with the `_manual` suffix to have access to manual pagination. There are three new examples for this, check out `examples/pagination*` to learn more! +- ([#166](https://github.com/ramsayleung/rspotify/pull/166) [#201](https://github.com/ramsayleung/rspotify/pull/201)) Add automatic pagination, which is now enabled by default. You can still use the methods with the `_manual` suffix to have access to manual pagination. There are three new examples for this, check out `examples/pagination*` to learn more! As a side effect, some methods now take references instead of values (so that they can be used multiple times when querying), and the parameters have been reordered so that the `limit` and `offset` are consistently the last two. The pagination chunk size can be configured with the `Spotify::pagination_chunks` field, which is set to 50 items by default. +- No default values are set from Rspotify now, they will be left to the Spotify API. +- ([#202](https://github.com/ramsayleung/rspotify/pull/202)) Add a `collaborative` parameter to `user_playlist_create`. +- ([#202](https://github.com/ramsayleung/rspotify/pull/202)) Add a `uris` parameter to `playlist_reorder_tracks`. ## 0.10 (2020/07/01) diff --git a/examples/current_user_recently_played.rs b/examples/current_user_recently_played.rs index 3001e137..d68b0eb1 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/current_user_recently_played.rs @@ -45,7 +45,7 @@ async fn main() { spotify.prompt_for_user_token().await.unwrap(); // Running the requests - let history = spotify.current_user_recently_played(10).await; + let history = spotify.current_user_recently_played(Some(10)).await; println!("Response: {:?}", history); } diff --git a/examples/pagination_manual.rs b/examples/pagination_manual.rs index a5804456..80b338fd 100644 --- a/examples/pagination_manual.rs +++ b/examples/pagination_manual.rs @@ -54,7 +54,7 @@ async fn main() { println!("Items:"); loop { let page = spotify - .current_user_saved_tracks_manual(limit, offset) + .current_user_saved_tracks_manual(Some(limit), Some(offset)) .await .unwrap(); for item in page.items { diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index fb322050..4154394f 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -1,8 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scope; - -use std::collections::HashSet; +use rspotify::scopes; fn main() { // You can use any logger for debugging. diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index 4667ac1d..e089c2fd 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -1,8 +1,6 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scope; - -use std::collections::HashSet; +use rspotify::scopes; fn main() { // You can use any logger for debugging. diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index 1f939f23..2a601877 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -1,9 +1,7 @@ use rspotify::client::SpotifyBuilder; use rspotify::model::{Country, Market, SearchType}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scope; - -use std::collections::HashSet; +use rspotify::scopes; fn main() { // You can use any logger for debugging. @@ -47,7 +45,7 @@ fn main() { spotify.request_client_token().unwrap(); let album_query = "album:arrival artist:abba"; - let result = spotify.search(album_query, SearchType::Album, 10, 0, None, None); + let result = spotify.search(album_query, SearchType::Album, None, None, Some(10), None); match result { Ok(album) => println!("searched album:{:?}", album), Err(err) => println!("search error!{:?}", err), @@ -57,10 +55,10 @@ fn main() { let result = spotify.search( artist_query, SearchType::Artist, - 10, - 0, Some(Market::Country(Country::UnitedStates)), None, + Some(10), + None, ); match result { Ok(album) => println!("searched artist:{:?}", album), @@ -71,10 +69,10 @@ fn main() { let result = spotify.search( playlist_query, SearchType::Playlist, - 10, - 0, Some(Market::Country(Country::UnitedStates)), None, + Some(10), + None, ); match result { Ok(album) => println!("searched playlist:{:?}", album), @@ -85,10 +83,10 @@ fn main() { let result = spotify.search( track_query, SearchType::Track, - 10, - 0, Some(Market::Country(Country::UnitedStates)), None, + Some(10), + None, ); match result { Ok(album) => println!("searched track:{:?}", album), @@ -96,14 +94,21 @@ fn main() { } let show_query = "love"; - let result = spotify.search(show_query, SearchType::Show, 10, 0, None, None); + let result = spotify.search(show_query, SearchType::Show, None, None, Some(10), None); match result { Ok(show) => println!("searched show:{:?}", show), Err(err) => println!("search error!{:?}", err), } let episode_query = "love"; - let result = spotify.search(episode_query, SearchType::Episode, 10, 0, None, None); + let result = spotify.search( + episode_query, + SearchType::Episode, + None, + None, + Some(10), + None, + ); match result { Ok(episode) => println!("searched episode:{:?}", episode), Err(err) => println!("search error!{:?}", err), diff --git a/src/client.rs b/src/client.rs index fa8d6319..cf716b30 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,10 +12,10 @@ use thiserror::Error; use std::path::PathBuf; use super::http::{HTTPClient, Query}; -use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; use super::pagination::{paginate, Paginator}; +use super::{build_json, build_map}; use crate::model::idtypes::{IdType, PlayContextIdType}; use std::collections::HashMap; @@ -154,7 +154,7 @@ impl Spotify { } /// Append device ID to an API path. - fn append_device_id(&self, path: &str, device_id: Option) -> String { + fn append_device_id(&self, path: &str, device_id: Option<&str>) -> String { let mut new_path = path.to_string(); if let Some(_device_id) = device_id { if path.contains('?') { @@ -190,14 +190,12 @@ impl Spotify { pub async fn tracks<'a>( &self, track_ids: impl IntoIterator, - market: Option, + market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(track_ids); - - let mut params = Query::new(); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; let url = format!("tracks/?ids={}", ids); let result = self.endpoint_get(&url, ¶ms).await?; @@ -252,7 +250,7 @@ impl Spotify { pub fn artist_albums<'a>( &'a self, artist_id: &'a ArtistId, - album_type: Option, + album_type: Option<&'a AlbumType>, market: Option<&'a Market>, ) -> impl Paginator> + 'a { paginate( @@ -268,26 +266,20 @@ impl Spotify { pub async fn artist_albums_manual( &self, artist_id: &ArtistId, - album_type: Option, + album_type: Option<&AlbumType>, market: Option<&Market>, limit: Option, offset: Option, ) -> ClientResult> { - let mut params = Query::new(); let limit = limit.map(|x| x.to_string()); - if let Some(ref limit) = limit { - params.insert("limit", limit); - } - if let Some(ref album_type) = album_type { - params.insert("album_type", album_type.as_ref()); - } let offset = offset.map(|x| x.to_string()); - if let Some(ref offset) = offset { - params.insert("offset", offset); - } - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + optional "album_type": album_type.map(|x| x.as_ref()), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let url = format!("artists/{}/albums", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -307,9 +299,9 @@ impl Spotify { artist_id: &ArtistId, market: Market, ) -> ClientResult> { - let mut params = Query::with_capacity(1); - - params.insert("market", market.as_ref()); + let params = build_map! { + "market": market.as_ref() + }; let url = format!("artists/{}/top-tracks", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; @@ -382,28 +374,25 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#category-search) #[maybe_async] - pub async fn search>, O: Into>>( + pub async fn search( &self, q: &str, _type: SearchType, - limit: L, - offset: O, - market: Option, - include_external: Option, + market: Option<&Market>, + include_external: Option<&IncludeExternal>, + limit: Option, + offset: Option, ) -> ClientResult { - let mut params = Query::with_capacity(4); - let limit = limit.into().unwrap_or(10).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - params.insert("q", q); - params.insert("type", _type.as_ref()); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } - if let Some(ref include_external) = include_external { - params.insert("include_external", include_external.as_ref()); - } + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + "q": q, + "type": _type.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "include_external": include_external.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; let result = self.endpoint_get("search", ¶ms).await?; self.convert_result(&result) @@ -432,17 +421,19 @@ impl Spotify { /// The manually paginated version of [`Spotify::album_track`]. #[maybe_async] - pub async fn album_track_manual>, O: Into>>( + pub async fn album_track_manual( &self, album_id: &AlbumId, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(50).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let url = format!("albums/{}/tracks", album_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -473,15 +464,12 @@ impl Spotify { &self, playlist_id: &PlaylistId, fields: Option<&str>, - market: Option, + market: Option<&Market>, ) -> ClientResult { - let mut params = Query::new(); - if let Some(fields) = fields { - params.insert("fields", fields); - } - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + }; let url = format!("playlists/{}", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; @@ -507,16 +495,17 @@ impl Spotify { /// The manually paginated version of [`Spotify::current_user_playlists`]. #[maybe_async] - pub async fn current_user_playlists_manual>, O: Into>>( + pub async fn current_user_playlists_manual( &self, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(50).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; let result = self.endpoint_get("me/playlists", ¶ms).await?; self.convert_result(&result) @@ -545,17 +534,19 @@ impl Spotify { /// The manually paginated version of [`Spotify::user_playlists`]. #[maybe_async] - pub async fn user_playlists_manual>, O: Into>>( + pub async fn user_playlists_manual( &self, user_id: &UserId, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(50).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -576,22 +567,16 @@ impl Spotify { playlist_id: Option<&PlaylistId>, fields: Option<&str>, ) -> ClientResult { - let mut params = Query::new(); - if let Some(fields) = fields { - params.insert("fields", fields); - } - match playlist_id { - Some(playlist_id) => { - let url = format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - None => { - let url = format!("users/{}/starred", user_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - } + let params = build_map! { + optional "fields": fields, + }; + + let url = match playlist_id { + Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), + None => format!("users/{}/starred", user_id.id()), + }; + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) } /// Get full details of the tracks of a playlist owned by a user. @@ -623,25 +608,23 @@ impl Spotify { /// The manually paginated version of [`Spotify::playlist_tracks`]. #[maybe_async] - pub async fn playlist_tracks_manual>, O: Into>>( + pub async fn playlist_tracks_manual( &self, playlist_id: &PlaylistId, fields: Option<&str>, market: Option<&Market>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(50).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } - if let Some(fields) = fields { - params.insert("fields", fields); - } + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -654,23 +637,25 @@ impl Spotify { /// - name - the name of the playlist /// - public - is the created playlist public /// - description - the description of the playlist + /// - collaborative - if the playlist will be collaborative /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist) #[maybe_async] - pub async fn user_playlist_create>, D: Into>>( + pub async fn user_playlist_create( &self, user_id: &UserId, name: &str, - public: P, - description: D, + public: Option, + collaborative: Option, + description: Option<&str>, ) -> ClientResult { - let public = public.into().unwrap_or(true); - let description = description.into().unwrap_or_else(|| "".to_owned()); - let params = json!({ + let params = build_json! { "name": name, - "public": public, - "description": description - }); + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; self.convert_result(&result) @@ -692,22 +677,16 @@ impl Spotify { playlist_id: &str, name: Option<&str>, public: Option, - description: Option, + description: Option<&str>, collaborative: Option, ) -> ClientResult { - let mut params = json!({}); - if let Some(name) = name { - json_insert!(params, "name", name); - } - if let Some(public) = public { - json_insert!(params, "public", public); - } - if let Some(collaborative) = collaborative { - json_insert!(params, "collaborative", collaborative); - } - if let Some(description) = description { - json_insert!(params, "description", description); - } + let params = build_json! { + optional "name": name, + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + let url = format!("playlists/{}", playlist_id); self.endpoint_put(&url, ¶ms).await } @@ -740,11 +719,11 @@ impl Spotify { position: Option, ) -> ClientResult { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris, + optional "position": position, + }; - let mut params = json!({ "uris": uris }); - if let Some(position) = position { - json_insert!(params, "position", position); - } let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; self.convert_result(&result) @@ -765,8 +744,10 @@ impl Spotify { track_ids: impl IntoIterator, ) -> ClientResult<()> { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris + }; - let params = json!({ "uris": uris }); let url = format!("playlists/{}/tracks", playlist_id.id()); self.endpoint_put(&url, ¶ms).await?; @@ -777,30 +758,32 @@ impl Spotify { /// /// Parameters: /// - playlist_id - the id of the playlist + /// - uris - a list of Spotify URIs to replace or clear /// - range_start - the position of the first track to be reordered + /// - insert_before - the position where the tracks should be inserted /// - range_length - optional the number of tracks to be reordered (default: /// 1) - /// - insert_before - the position where the tracks should be inserted /// - snapshot_id - optional playlist's snapshot ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) #[maybe_async] - pub async fn playlist_reorder_tracks>>( + pub async fn playlist_reorder_tracks( &self, playlist_id: &PlaylistId, - range_start: i32, - range_length: R, - insert_before: i32, - snapshot_id: Option, + uris: Option<&[&Id]>, + range_start: Option, + insert_before: Option, + range_length: Option, + snapshot_id: Option<&str>, ) -> ClientResult { - let mut params = json! ({ - "range_start": range_start, - "range_length": range_length.into().unwrap_or(1), - "insert_before": insert_before - }); - if let Some(snapshot_id) = snapshot_id { - json_insert!(params, "snapshot_id", snapshot_id); - } + let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); + let params = build_json! { + optional "uris": uris, + optional "range_start": range_start, + optional "insert_before": insert_before, + optional "range_length": range_length, + optional "snapshot_id": snapshot_id, + }; let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_put(&url, ¶ms).await?; @@ -820,7 +803,7 @@ impl Spotify { &self, playlist_id: &PlaylistId, track_ids: impl IntoIterator, - snapshot_id: Option, + snapshot_id: Option<&str>, ) -> ClientResult { let tracks = track_ids .into_iter() @@ -831,11 +814,10 @@ impl Spotify { }) .collect::>(); - let mut params = json!({ "tracks": tracks }); - - if let Some(snapshot_id) = snapshot_id { - json_insert!(params, "snapshot_id", snapshot_id); - } + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; @@ -876,9 +858,9 @@ impl Spotify { &self, playlist_id: &PlaylistId, tracks: Vec>, - snapshot_id: Option, + snapshot_id: Option<&str>, ) -> ClientResult { - let ftracks = tracks + let tracks = tracks .into_iter() .map(|track| { let mut map = Map::new(); @@ -888,10 +870,11 @@ impl Spotify { }) .collect::>(); - let mut params = json!({ "tracks": ftracks }); - if let Some(snapshot_id) = snapshot_id { - json_insert!(params, "snapshot_id", snapshot_id); - } + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; self.convert_result(&result) @@ -904,20 +887,18 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-playlist) #[maybe_async] - pub async fn playlist_follow>>( + pub async fn playlist_follow( &self, playlist_id: &PlaylistId, - public: P, + public: Option, ) -> ClientResult<()> { let url = format!("playlists/{}/followers", playlist_id.id()); - self.endpoint_put( - &url, - &json! ({ - "public": public.into().unwrap_or(true) - }), - ) - .await?; + let params = build_json! { + optional "public": public, + }; + + self.endpoint_put(&url, ¶ms).await?; Ok(()) } @@ -931,10 +912,10 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) #[maybe_async] - pub async fn playlist_check_follow<'a>( + pub async fn playlist_check_follow( &self, playlist_id: &PlaylistId, - user_ids: &'a [&'a UserId], + user_ids: &[&UserId], ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); @@ -1010,18 +991,18 @@ impl Spotify { /// The manually paginated version of /// [`Spotify::current_user_saved_albums`]. #[maybe_async] - pub async fn current_user_saved_albums_manual>, O: Into>>( + pub async fn current_user_saved_albums_manual( &self, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - // TODO: we should use the API's default value instead of - // `.unwrap_or(20)` and similars. - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get("me/albums", ¶ms).await?; self.convert_result(&result) } @@ -1040,7 +1021,7 @@ impl Spotify { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) pub fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { paginate( - move |limit, offset| self.current_user_saved_tracks_manual(limit, offset), + move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), self.pagination_chunks, ) } @@ -1048,16 +1029,18 @@ impl Spotify { /// The manually paginated version of /// [`Spotify::current_user_saved_tracks`]. #[maybe_async] - pub async fn current_user_saved_tracks_manual>, O: Into>>( + pub async fn current_user_saved_tracks_manual( &self, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get("me/tracks", ¶ms).await?; self.convert_result(&result) } @@ -1065,23 +1048,22 @@ impl Spotify { /// Gets a list of the artists followed by the current authorized user. /// /// Parameters: - /// - limit - the number of tracks to return /// - after - the last artist ID retrieved from the previous request + /// - limit - the number of tracks to return /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-followed) #[maybe_async] - pub async fn current_user_followed_artists>>( + pub async fn current_user_followed_artists( &self, - limit: L, - after: Option, + after: Option<&str>, + limit: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - params.insert("limit", &limit); - params.insert("type", Type::Artist.as_ref()); - if let Some(ref after) = after { - params.insert("after", &after); - } + let limit = limit.map(|s| s.to_string()); + let params = build_map! { + "type": Type::Artist.as_ref(), + optional "after": after, + optional "limit": limit.as_deref(), + }; let result = self.endpoint_get("me/following", ¶ms).await?; self.convert_result::(&result) @@ -1155,31 +1137,29 @@ impl Spotify { time_range: Option<&'a TimeRange>, ) -> impl Paginator> + 'a { paginate( - move |limit, offset| self.current_user_top_artists_manual(time_range, limit, offset), + move |limit, offset| { + self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) + }, self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::current_user_top_artists`]. #[maybe_async] - pub async fn current_user_top_artists_manual< - 'a, - T: Into>, - L: Into>, - O: Into>, - >( - &'a self, - time_range: T, - limit: L, - offset: O, + pub async fn current_user_top_artists_manual( + &self, + time_range: Option<&TimeRange>, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(3); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - let time_range = time_range.into().unwrap_or(&TimeRange::MediumTerm); - params.insert("limit", &limit); - params.insert("offset", &offset); - params.insert("time_range", time_range.as_ref()); + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; self.convert_result(&result) } @@ -1200,26 +1180,29 @@ impl Spotify { time_range: Option<&'a TimeRange>, ) -> impl Paginator> + 'a { paginate( - move |limit, offset| self.current_user_top_tracks_manual(time_range, limit, offset), + move |limit, offset| { + self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) + }, self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::current_user_top_tracks`]. #[maybe_async] - pub async fn current_user_top_tracks_manual>, O: Into>>( + pub async fn current_user_top_tracks_manual( &self, time_range: Option<&TimeRange>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(3); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - let time_range = time_range.unwrap_or(&TimeRange::MediumTerm); - params.insert("limit", &limit); - params.insert("offset", &offset); - params.insert("time_range", time_range.as_ref()); + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get("me/top/tracks", ¶ms).await?; self.convert_result(&result) } @@ -1231,13 +1214,15 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track) #[maybe_async] - pub async fn current_user_recently_played>>( + pub async fn current_user_recently_played( &self, - limit: L, + limit: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(1); - let limit = limit.into().unwrap_or(50).to_string(); - params.insert("limit", &limit); + let limit = limit.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + }; + let result = self .endpoint_get("me/player/recently-played", ¶ms) .await?; @@ -1401,29 +1386,25 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-featured-playlists) #[maybe_async] - pub async fn featured_playlists>, O: Into>>( + pub async fn featured_playlists( &self, - locale: Option, - country: Option, + locale: Option<&str>, + country: Option<&Market>, timestamp: Option>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(ref locale) = locale { - params.insert("locale", locale); - } - if let Some(ref market) = country { - params.insert("country", market.as_ref()); - } + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); let timestamp = timestamp.map(|x| x.to_rfc3339()); - if let Some(ref timestamp) = timestamp { - params.insert("timestamp", timestamp); - } + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "timestamp": timestamp.as_deref(), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self .endpoint_get("browse/featured-playlists", ¶ms) .await?; @@ -1448,27 +1429,26 @@ impl Spotify { country: Option<&'a Market>, ) -> impl Paginator> + 'a { paginate( - move |limit, offset| self.new_releases_manual(country, limit, offset), + move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::new_releases`]. #[maybe_async] - pub async fn new_releases_manual>, O: Into>>( + pub async fn new_releases_manual( &self, country: Option<&Market>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(ref market) = country { - params.insert("country", market.as_ref()); - } + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; let result = self.endpoint_get("browse/new-releases", ¶ms).await?; self.convert_result::(&result) @@ -1496,31 +1476,28 @@ impl Spotify { country: Option<&'a Market>, ) -> impl Paginator> + 'a { paginate( - move |limit, offset| self.categories_manual(locale, country, limit, offset), + move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::categories`]. #[maybe_async] - pub async fn categories_manual>, O: Into>>( + pub async fn categories_manual( &self, locale: Option<&str>, country: Option<&Market>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(locale) = locale { - params.insert("locale", locale); - } - if let Some(ref market) = country { - params.insert("country", market.as_ref()); - } + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; let result = self.endpoint_get("browse/categories", ¶ms).await?; self.convert_result::(&result) .map(|x| x.categories) @@ -1547,7 +1524,7 @@ impl Spotify { ) -> impl Paginator> + 'a { paginate( move |limit, offset| { - self.category_playlists_manual(category_id, country, limit, offset) + self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) }, self.pagination_chunks, ) @@ -1555,21 +1532,20 @@ impl Spotify { /// The manually paginated version of [`Spotify::category_playlists`]. #[maybe_async] - pub async fn category_playlists_manual>, O: Into>>( + pub async fn category_playlists_manual( &self, category_id: &str, country: Option<&Market>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(ref market) = country { - params.insert("country", market.as_ref()); - } + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; let url = format!("browse/categories/{}/playlists", category_id); let result = self.endpoint_get(&url, ¶ms).await?; @@ -1593,18 +1569,27 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recommendations) #[maybe_async] - pub async fn recommendations>>( + pub async fn recommendations( &self, + payload: &Map, seed_artists: Option>, - seed_genres: Option>, + seed_genres: Option>, seed_tracks: Option>, - limit: L, - market: Option, - payload: &Map, + market: Option<&Market>, + limit: Option, ) -> ClientResult { - let mut params = Query::with_capacity(payload.len() + 1); - let limit = limit.into().unwrap_or(20).to_string(); - params.insert("limit", &limit); + let seed_artists = seed_artists.map(join_ids); + let seed_genres = seed_genres.map(|x| x.join(",")); + let seed_tracks = seed_tracks.map(join_ids); + let limit = limit.map(|x| x.to_string()); + let mut params = build_map! { + optional "seed_artists": seed_artists.as_ref(), + optional "seed_genres": seed_genres.as_ref(), + optional "seed_tracks": seed_tracks.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + }; + // TODO: this probably can be improved. let attributes = [ "acousticness", @@ -1639,24 +1624,6 @@ impl Spotify { params.insert(key, value); } - let seed_artists = seed_artists.map(join_ids); - if let Some(ref seed_artists) = seed_artists { - params.insert("seed_artists", seed_artists); - } - - let seed_genres = seed_genres.map(|x| x.join(",")); - if let Some(ref seed_genres) = seed_genres { - params.insert("seed_genres", seed_genres); - } - - let seed_tracks = seed_tracks.map(join_ids); - if let Some(ref seed_tracks) = seed_tracks { - params.insert("seed_tracks", seed_tracks); - } - - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } let result = self.endpoint_get("recommendations", ¶ms).await?; self.convert_result(&result) } @@ -1731,21 +1698,21 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-information-about-the-users-current-playback) #[maybe_async] - pub async fn current_playback( + pub async fn current_playback<'a, A: IntoIterator>( &self, - market: Option, - additional_types: Option>, + country: Option<&Market>, + additional_types: Option, ) -> ClientResult> { - let mut params = Query::new(); - if let Some(ref market) = market { - params.insert("country", market.as_ref()); - } - let additional_types = - additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - - if let Some(ref additional_types) = additional_types { - params.insert("additional_types", additional_types); - } + let additional_types = additional_types.map(|x| { + x.into_iter() + .map(|x| x.as_ref()) + .collect::>() + .join(",") + }); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_deref(), + }; let result = self.endpoint_get("me/player", ¶ms).await?; if result.is_empty() { @@ -1767,18 +1734,15 @@ impl Spotify { #[maybe_async] pub async fn current_playing( &self, - market: Option, + market: Option<&Market>, additional_types: Option>, ) -> ClientResult> { - let mut params = Query::new(); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } let additional_types = additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - if let Some(ref additional_types) = additional_types { - params.insert("additional_types", additional_types); - } + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_ref(), + }; let result = self .get("me/player/currently-playing", None, ¶ms) @@ -1797,25 +1761,17 @@ impl Spotify { /// /// Parameters: /// - device_id - transfer playback to this device - /// - force_play - true: after transfer, play. false: - /// keep current state. + /// - force_play - true: after transfer, play. false: keep current state. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-transfer-a-users-playback) #[maybe_async] - pub async fn transfer_playback>>( - &self, - device_id: &str, - force_play: T, - ) -> ClientResult<()> { - self.endpoint_put( - "me/player", - &json! ({ - "device_ids": vec![device_id.to_owned()], - "play": force_play.into().unwrap_or(true) - }), - ) - .await?; + pub async fn transfer_playback(&self, device_id: &str, play: Option) -> ClientResult<()> { + let params = build_json! { + "device_ids": [device_id], + optional "play": play, + }; + self.endpoint_put("me/player", ¶ms).await?; Ok(()) } @@ -1835,30 +1791,25 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) #[maybe_async] - pub async fn start_context_playback( + pub async fn start_context_playback( &self, - context_uri: &Id, - device_id: Option, - offset: Option>, + context_uri: &Id, + device_id: Option<&str>, + 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 { - match offset { - Offset::Position(position) => { - json_insert!(params, "offset", json!({ "position": position })); - } - Offset::Uri(uri) => { - json_insert!(params, "offset", json!({ "uri": uri.uri() })); - } - } - } - if let Some(position_ms) = position_ms { - json_insert!(params, "position_ms", position_ms); + let params = build_json! { + "context_uri": context_uri.uri(), + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), + optional "position_ms": position_ms, + }; + let url = self.append_device_id("me/player/play", device_id); self.put(&url, None, ¶ms).await?; @@ -1869,31 +1820,21 @@ impl Spotify { pub async fn start_uris_playback( &self, uris: &[&Id], - device_id: Option, + device_id: Option<&str>, offset: Option>, position_ms: Option, ) -> ClientResult<()> { use super::model::Offset; - let mut params = json!({}); - json_insert!( - params, - "uris", - uris.iter().map(|id| id.uri()).collect::>() - ); - if let Some(offset) = offset { - match offset { - Offset::Position(position) => { - json_insert!(params, "offset", json!({ "position": position })); - } - Offset::Uri(uri) => { - json_insert!(params, "offset", json!({ "uri": uri.uri() })); - } - } - } - if let Some(position_ms) = position_ms { - json_insert!(params, "position_ms", position_ms); + let params = build_json! { + "uris": uris.iter().map(|id| id.uri()).collect::>(), + optional "position_ms": position_ms, + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), }; + let url = self.append_device_id("me/player/play", device_id); self.endpoint_put(&url, ¶ms).await?; @@ -1907,7 +1848,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) #[maybe_async] - pub async fn pause_playback(&self, device_id: Option) -> ClientResult<()> { + pub async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id("me/player/pause", device_id); self.endpoint_put(&url, &json!({})).await?; @@ -1921,7 +1862,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) #[maybe_async] - pub async fn next_track(&self, device_id: Option) -> ClientResult<()> { + pub async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id("me/player/next", device_id); self.endpoint_post(&url, &json!({})).await?; @@ -1935,7 +1876,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) #[maybe_async] - pub async fn previous_track(&self, device_id: Option) -> ClientResult<()> { + pub async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id("me/player/previous", device_id); self.endpoint_post(&url, &json!({})).await?; @@ -1950,11 +1891,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) #[maybe_async] - pub async fn seek_track( - &self, - position_ms: u32, - device_id: Option, - ) -> ClientResult<()> { + pub async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/seek?position_ms={}", position_ms), device_id, @@ -1972,7 +1909,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) #[maybe_async] - pub async fn repeat(&self, state: RepeatState, device_id: Option) -> ClientResult<()> { + pub async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/repeat?state={}", state.as_ref()), device_id, @@ -1990,7 +1927,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) #[maybe_async] - pub async fn volume(&self, volume_percent: u8, device_id: Option) -> ClientResult<()> { + pub async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { if volume_percent > 100u8 { error!("volume must be between 0 and 100, inclusive"); } @@ -2011,7 +1948,7 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) #[maybe_async] - pub async fn shuffle(&self, state: bool, device_id: Option) -> ClientResult<()> { + pub async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); self.endpoint_put(&url, &json!({})).await?; @@ -2028,10 +1965,10 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) #[maybe_async] - pub async fn add_item_to_queue( + pub async fn add_item_to_queue( &self, - item: &Id, - device_id: Option, + item: &Id, + device_id: Option<&str>, ) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); self.endpoint_post(&url, &json!({})).await?; @@ -2072,23 +2009,25 @@ impl Spotify { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) pub fn get_saved_show(&self) -> impl Paginator> + '_ { paginate( - move |limit, offset| self.get_saved_show_manual(limit, offset), + move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::get_saved_show`]. #[maybe_async] - pub async fn get_saved_show_manual>, O: Into>>( + pub async fn get_saved_show_manual( &self, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + let result = self.endpoint_get("me/shows", ¶ms).await?; self.convert_result(&result) } @@ -2103,11 +2042,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) #[maybe_async] - pub async fn get_a_show(&self, id: &ShowId, market: Option) -> ClientResult { - let mut params = Query::new(); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + pub async fn get_a_show(&self, id: &ShowId, market: Option<&Market>) -> ClientResult { + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + let url = format!("shows/{}", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -2125,14 +2064,14 @@ impl Spotify { pub async fn get_several_shows<'a>( &self, ids: impl IntoIterator, - market: Option, + market: Option<&Market>, ) -> ClientResult> { - let mut params = Query::with_capacity(1); let ids = join_ids(ids); - params.insert("ids", ids.as_ref()); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; + let result = self.endpoint_get("shows", ¶ms).await?; self.convert_result::(&result) .map(|x| x.shows) @@ -2159,28 +2098,30 @@ impl Spotify { market: Option<&'a Market>, ) -> impl Paginator> + 'a { paginate( - move |limit, offset| self.get_shows_episodes_manual(id, market, limit, offset), + move |limit, offset| { + self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) + }, self.pagination_chunks, ) } /// The manually paginated version of [`Spotify::get_shows_episodes`]. #[maybe_async] - pub async fn get_shows_episodes_manual>, O: Into>>( + pub async fn get_shows_episodes_manual( &self, id: &ShowId, market: Option<&Market>, - limit: L, - offset: O, + limit: Option, + offset: Option, ) -> ClientResult> { - let mut params = Query::with_capacity(2); - let limit = limit.into().unwrap_or(20).to_string(); - let offset = offset.into().unwrap_or(0).to_string(); - params.insert("limit", &limit); - params.insert("offset", &offset); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + let url = format!("shows/{}/episodes", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -2199,13 +2140,12 @@ impl Spotify { pub async fn get_an_episode( &self, id: &EpisodeId, - market: Option, + market: Option<&Market>, ) -> ClientResult { let url = format!("episodes/{}", id.id()); - let mut params = Query::new(); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) @@ -2222,14 +2162,13 @@ impl Spotify { pub async fn get_several_episodes<'a>( &self, ids: impl IntoIterator, - market: Option, + market: Option<&Market>, ) -> ClientResult> { - let mut params = Query::with_capacity(1); let ids = join_ids(ids); - params.insert("ids", ids.as_ref()); - if let Some(ref market) = market { - params.insert("market", market.as_ref()); - } + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; let result = self.endpoint_get("episodes", ¶ms).await?; self.convert_result::(&result) @@ -2247,9 +2186,10 @@ impl Spotify { &self, ids: impl IntoIterator, ) -> ClientResult> { - let mut params = Query::with_capacity(1); let ids = join_ids(ids); - params.insert("ids", ids.as_str()); + let params = build_map! { + "ids": &ids, + }; let result = self.endpoint_get("me/shows/contains", ¶ms).await?; self.convert_result(&result) } @@ -2266,13 +2206,12 @@ impl Spotify { pub async fn remove_users_saved_shows<'a>( &self, show_ids: impl IntoIterator, - market: Option, + country: Option<&Market>, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); - let mut params = json!({}); - if let Some(market) = market { - json_insert!(params, "country", market.as_ref()); - } + let params = build_json! { + optional "country": country.map(|x| x.as_ref()) + }; self.endpoint_delete(&url, ¶ms).await?; Ok(()) @@ -2299,7 +2238,7 @@ mod test { #[test] fn test_append_device_id_without_question_mark() { let path = "me/player/play"; - let device_id = Some("fdafdsadfa".to_owned()); + let device_id = Some("fdafdsadfa"); let spotify = SpotifyBuilder::default().build().unwrap(); let new_path = spotify.append_device_id(path, device_id); assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); @@ -2308,7 +2247,7 @@ mod test { #[test] fn test_append_device_id_with_question_mark() { let path = "me/player/shuffle?state=true"; - let device_id = Some("fdafdsadfa".to_owned()); + let device_id = Some("fdafdsadfa"); let spotify = SpotifyBuilder::default().build().unwrap(); let new_path = spotify.append_device_id(path, device_id); assert_eq!( diff --git a/src/macros.rs b/src/macros.rs index c3dbb091..2e855f0d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -33,38 +33,176 @@ macro_rules! scopes { }}; } -/// Reduce boilerplate when inserting new elements in a JSON object. +/// Count items in a list of items within a macro, taken from here: +/// https://danielkeep.github.io/tlborm/book/blk-counting.html #[doc(hidden)] #[macro_export] -macro_rules! json_insert { - ($json:expr, $p1:expr, $p2:expr) => { - $json - .as_object_mut() - .unwrap() - .insert($p1.to_string(), json!($p2)) +macro_rules! replace_expr { + ($_t:tt $sub:expr) => { + $sub }; } +#[doc(hidden)] +#[macro_export] +macro_rules! count_items { + ($($item:expr),*) => {<[()]>::len(&[$($crate::replace_expr!($item ())),*])}; +} + +/// This macro and [`build_json`] help make the endpoints as concise as possible +/// and boilerplate-free, which is specially important when initializing the +/// parameters of the query. In the case of `build_map` this will construct a +/// `HashMap<&str, &str>`, and `build_json` will initialize a +/// `HashMap`. +/// +/// The syntax is the following: +/// +/// [optional] "key": value +/// +/// For an example, refer to the `test::test_build_map` function in this module, +/// or the real usages in Rspotify's client. +/// +/// The `key` and `value` parameters are what's to be inserted in the HashMap. +/// If `optional` is used, the value will only be inserted if it's a +/// `Some(...)`. +#[doc(hidden)] +#[macro_export] +macro_rules! internal_build_map { + (/* required */, $map:ident, $key:expr, $val:expr) => { + $map.insert($key, $val); + }; + (optional, $map:ident, $key:expr, $val:expr) => { + if let Some(val) = $val { + $map.insert($key, val); + } + }; +} +#[doc(hidden)] +#[macro_export] +macro_rules! build_map { + ( + $( + $( $kind:ident )? $key:literal : $val:expr + ),+ $(,)? + ) => {{ + let mut params = $crate::http::Query::with_capacity( + $crate::count_items!($( $key ),*) + ); + $( + $crate::internal_build_map!( + $( $kind )?, + params, + $key, + $val + ); + )+ + params + }}; +} + +/// Refer to the [`build_map`] documentation; this is the same but for JSON +/// maps. +#[doc(hidden)] +#[macro_export] +macro_rules! internal_build_json { + (/* required */, $map:ident, $key:expr, $val:expr) => { + $map.insert($key.to_string(), json!($val)); + }; + (optional, $map:ident, $key:expr, $val:expr) => { + if let Some(val) = $val { + $map.insert($key.to_string(), json!(val)); + } + }; +} +#[doc(hidden)] +#[macro_export] +macro_rules! build_json { + ( + $( + $( $kind:ident )? $key:literal : $val:expr + ),+ $(,)? + ) => {{ + let mut params = ::serde_json::map::Map::with_capacity( + $crate::count_items!($( $key ),*) + ); + $( + $crate::internal_build_json!( + $( $kind )?, + params, + $key, + $val + ); + )+ + ::serde_json::Value::from(params) + }}; +} #[cfg(test)] mod test { - use crate::{json_insert, scopes}; - use serde_json::json; + use crate::http::Query; + use crate::model::Market; + use crate::{build_json, build_map, scopes}; + use serde_json::{json, Map, Value}; #[test] fn test_hashset() { let scope = scopes!("hello", "world", "foo", "bar"); assert_eq!(scope.len(), 4); - assert!(scope.contains(&"hello".to_owned())); - assert!(scope.contains(&"world".to_owned())); - assert!(scope.contains(&"foo".to_owned())); - assert!(scope.contains(&"bar".to_owned())); + assert!(scope.contains("hello")); + assert!(scope.contains("world")); + assert!(scope.contains("foo")); + assert!(scope.contains("bar")); + } + + #[test] + fn test_build_map() { + // Passed as parameters, for example. + let id = "Pink Lemonade"; + let artist = Some("The Wombats"); + let market: Option<&Market> = None; + + let with_macro = build_map! { + // Mandatory (not an `Option`) + "id": id, + // Can be used directly + optional "artist": artist, + // `Modality` needs to be converted to &str + optional "market": market.map(|x| x.as_ref()), + }; + + let mut manually = Query::with_capacity(3); + manually.insert("id", id); + if let Some(val) = artist { + manually.insert("artist", val); + } + if let Some(val) = market.map(|x| x.as_ref()) { + manually.insert("market", val); + } + + assert_eq!(with_macro, manually); } #[test] - fn test_json_insert() { - let mut params = json!({}); - let name = "ramsay"; - json_insert!(params, "name", name); - assert_eq!(params["name"], name); + fn test_json_query() { + // Passed as parameters, for example. + let id = "Pink Lemonade"; + let artist = Some("The Wombats"); + let market: Option<&Market> = None; + + let with_macro = build_json! { + "id": id, + optional "artist": artist, + optional "market": market.map(|x| x.as_ref()), + }; + + let mut manually = Map::with_capacity(3); + manually.insert("id".to_string(), json!(id)); + if let Some(val) = artist.map(|x| json!(x)) { + manually.insert("artist".to_string(), val); + } + if let Some(val) = market.map(|x| x.as_ref()).map(|x| json!(x)) { + manually.insert("market".to_string(), val); + } + + assert_eq!(with_macro, Value::from(manually)); } } diff --git a/src/model/enums/misc.rs b/src/model/enums/misc.rs index e7ee0dce..cb7e1211 100644 --- a/src/model/enums/misc.rs +++ b/src/model/enums/misc.rs @@ -102,11 +102,8 @@ pub enum Market { Country(Country), FromToken, } -pub trait AsRefStr { - fn as_ref(&self) -> &str; -} -impl AsRefStr for Market { +impl AsRef for Market { fn as_ref(&self) -> &str { match self { Market::Country(country) => country.as_ref(), diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 4df85a6c..e35e24d7 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -85,7 +85,7 @@ async fn test_artists_albums() { .await .artist_albums_manual( birdy_uri, - Some(AlbumType::Album), + Some(&AlbumType::Album), Some(&Market::Country(Country::UnitedStates)), Some(10), None, diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 0eba0c1c..73fedd5b 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -101,7 +101,12 @@ pub async fn oauth_client() -> Spotify { async fn test_categories() { oauth_client() .await - .categories_manual(None, Some(&Market::Country(Country::UnitedStates)), 10, 0) + .categories_manual( + None, + Some(&Market::Country(Country::UnitedStates)), + Some(10), + Some(0), + ) .await .unwrap(); } @@ -112,7 +117,12 @@ async fn test_categories() { async fn test_category_playlists() { oauth_client() .await - .category_playlists_manual("pop", Some(&Market::Country(Country::UnitedStates)), 10, 0) + .category_playlists_manual( + "pop", + Some(&Market::Country(Country::UnitedStates)), + Some(10), + Some(0), + ) .await .unwrap(); } @@ -123,7 +133,7 @@ async fn test_category_playlists() { async fn test_current_playback() { oauth_client() .await - .current_playback(None, None) + .current_playback::<&[_]>(None, None) .await .unwrap(); } @@ -145,7 +155,7 @@ async fn test_current_playing() { async fn test_current_user_followed_artists() { oauth_client() .await - .current_user_followed_artists(10, None) + .current_user_followed_artists(None, Some(10)) .await .unwrap(); } @@ -167,7 +177,7 @@ async fn test_current_user_playing_track() { async fn test_current_user_playlists() { oauth_client() .await - .current_user_playlists_manual(10, None) + .current_user_playlists_manual(Some(10), None) .await .unwrap(); } @@ -178,7 +188,7 @@ async fn test_current_user_playlists() { async fn test_current_user_recently_played() { oauth_client() .await - .current_user_recently_played(10) + .current_user_recently_played(Some(10)) .await .unwrap(); } @@ -221,7 +231,7 @@ async fn test_current_user_saved_albums_delete() { async fn test_current_user_saved_albums() { oauth_client() .await - .current_user_saved_albums_manual(10, 0) + .current_user_saved_albums_manual(Some(10), Some(0)) .await .unwrap(); } @@ -280,7 +290,7 @@ async fn test_current_user_saved_tracks_delete() { async fn test_current_user_saved_tracks() { oauth_client() .await - .current_user_saved_tracks_manual(10, 0) + .current_user_saved_tracks_manual(Some(10), Some(0)) .await .unwrap(); } @@ -291,7 +301,7 @@ async fn test_current_user_saved_tracks() { async fn test_current_user_top_artists() { oauth_client() .await - .current_user_top_artists_manual(Some(&TimeRange::ShortTerm), 10, 0) + .current_user_top_artists_manual(Some(&TimeRange::ShortTerm), Some(10), Some(0)) .await .unwrap(); } @@ -302,7 +312,7 @@ async fn test_current_user_top_artists() { async fn test_current_user_top_tracks() { oauth_client() .await - .current_user_top_tracks_manual(Some(&TimeRange::ShortTerm), 10, 0) + .current_user_top_tracks_manual(Some(&TimeRange::ShortTerm), Some(10), Some(0)) .await .unwrap(); } @@ -321,7 +331,7 @@ async fn test_featured_playlists() { let now: DateTime = Utc::now(); oauth_client() .await - .featured_playlists(None, None, Some(now), 10, 0) + .featured_playlists(None, None, Some(now), Some(10), Some(0)) .await .unwrap(); } @@ -339,7 +349,7 @@ async fn test_me() { async fn test_new_releases() { oauth_client() .await - .new_releases_manual(Some(&Market::Country(Country::Sweden)), 10, 0) + .new_releases_manual(Some(&Market::Country(Country::Sweden)), Some(10), Some(0)) .await .unwrap(); } @@ -350,7 +360,7 @@ async fn test_new_releases() { async fn test_new_releases_with_from_token() { oauth_client() .await - .new_releases_manual(Some(&Market::FromToken), 10, 0) + .new_releases_manual(Some(&Market::FromToken), Some(10), Some(0)) .await .unwrap(); } @@ -359,7 +369,7 @@ async fn test_new_releases_with_from_token() { #[maybe_async_test] #[ignore] async fn test_next_playback() { - let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); + let device_id = "74ASZWbe4lXaubB36ztrGX"; oauth_client() .await .next_track(Some(device_id)) @@ -371,7 +381,7 @@ async fn test_next_playback() { #[maybe_async_test] #[ignore] async fn test_pause_playback() { - let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); + let device_id = "74ASZWbe4lXaubB36ztrGX"; oauth_client() .await .pause_playback(Some(device_id)) @@ -383,7 +393,7 @@ async fn test_pause_playback() { #[maybe_async_test] #[ignore] async fn test_previous_playback() { - let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); + let device_id = "74ASZWbe4lXaubB36ztrGX"; oauth_client() .await .previous_track(Some(device_id)) @@ -403,12 +413,12 @@ async fn test_recommendations() { oauth_client() .await .recommendations( + &payload, Some(seed_artists), None, Some(seed_tracks), - 10, - Some(Market::Country(Country::UnitedStates)), - &payload, + Some(&Market::Country(Country::UnitedStates)), + Some(10), ) .await .unwrap(); @@ -432,7 +442,7 @@ async fn test_search_album() { let query = "album:arrival artist:abba"; oauth_client() .await - .search(query, SearchType::Album, 10, 0, None, None) + .search(query, SearchType::Album, None, None, Some(10), Some(0)) .await .unwrap(); } @@ -447,10 +457,10 @@ async fn test_search_artist() { .search( query, SearchType::Artist, - 10, - 0, - Some(Market::Country(Country::UnitedStates)), + Some(&Market::Country(Country::UnitedStates)), None, + Some(10), + Some(0), ) .await .unwrap(); @@ -466,10 +476,10 @@ async fn test_search_playlist() { .search( query, SearchType::Playlist, - 10, - 0, - Some(Market::Country(Country::UnitedStates)), + Some(&Market::Country(Country::UnitedStates)), None, + Some(10), + Some(0), ) .await .unwrap(); @@ -485,10 +495,10 @@ async fn test_search_track() { .search( query, SearchType::Track, - 10, - 0, - Some(Market::Country(Country::UnitedStates)), + Some(&Market::Country(Country::UnitedStates)), None, + Some(10), + Some(0), ) .await .unwrap(); @@ -512,7 +522,7 @@ async fn test_shuffle() { #[maybe_async_test] #[ignore] async fn test_start_playback() { - let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); + let device_id = "74ASZWbe4lXaubB36ztrGX"; let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await @@ -528,7 +538,7 @@ async fn test_transfer_playback() { let device_id = "74ASZWbe4lXaubB36ztrGX"; oauth_client() .await - .transfer_playback(device_id, true) + .transfer_playback(device_id, Some(true)) .await .unwrap(); } @@ -644,7 +654,7 @@ async fn test_user_playlist_create() { let playlist_name = "A New Playlist"; oauth_client() .await - .user_playlist_create(user_id, playlist_name, false, None) + .user_playlist_create(user_id, playlist_name, Some(false), None, None) .await .unwrap(); } @@ -656,7 +666,7 @@ async fn test_playlist_follow_playlist() { let playlist_id = Id::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); oauth_client() .await - .playlist_follow(playlist_id, true) + .playlist_follow(playlist_id, Some(true)) .await .unwrap(); } @@ -665,13 +675,21 @@ async fn test_playlist_follow_playlist() { #[maybe_async_test] #[ignore] async fn test_playlist_recorder_tracks() { + let uris: Option<&[&EpisodeId]> = None; let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let range_start = 0; let insert_before = 1; let range_length = 1; oauth_client() .await - .playlist_reorder_tracks(playlist_id, range_start, range_length, insert_before, None) + .playlist_reorder_tracks( + playlist_id, + uris, + Some(range_start), + Some(insert_before), + Some(range_length), + None, + ) .await .unwrap(); }