spotify_cli/http/
endpoints.rs

1//! Type-safe Spotify API endpoint paths
2//!
3//! This module provides a typed enum for all Spotify API endpoints,
4//! eliminating string formatting errors and providing a single source
5//! of truth for endpoint paths.
6//!
7//! # Design Pattern
8//!
9//! The `Endpoint` enum represents **URL paths**, not operations.
10//! Multiple endpoint wrapper files in `src/endpoints/` can share
11//! the same variant when they use different HTTP methods:
12//!
13//! - `Endpoint::PlaylistTracks` → GET (get_playlist_items), POST (add_items), DELETE (remove_items)
14//! - `Endpoint::PlayerState` → GET (get_playback_state), PUT (transfer_playback)
15//! - `Endpoint::SavedTracksIds` → PUT (save_tracks), DELETE (remove_tracks)
16//!
17//! This follows REST conventions: same URL, different HTTP verbs for different operations.
18
19use urlencoding::encode;
20
21/// Spotify API endpoint paths
22pub enum Endpoint<'a> {
23    PlayerState,
24    PlayerCurrentlyPlaying,
25    PlayerPlay,
26    PlayerPause,
27    PlayerNext,
28    PlayerPrevious,
29    PlayerQueue,
30    PlayerQueueAdd {
31        uri: &'a str,
32    },
33    PlayerDevices,
34    PlayerRecentlyPlayed,
35    PlayerSeek {
36        position_ms: u64,
37    },
38    PlayerVolume {
39        volume_percent: u8,
40    },
41    PlayerShuffle {
42        state: bool,
43    },
44    PlayerRepeat {
45        state: &'a str,
46    },
47
48    Playlist {
49        id: &'a str,
50    },
51    PlaylistTracks {
52        id: &'a str,
53    },
54    PlaylistItems {
55        id: &'a str,
56        limit: u8,
57        offset: u32,
58    },
59    PlaylistFollowers {
60        id: &'a str,
61    },
62    PlaylistCoverImage {
63        id: &'a str,
64    },
65    CurrentUserPlaylists {
66        limit: u8,
67        offset: u32,
68    },
69    UserPlaylists {
70        user_id: &'a str,
71    },
72    CategoryPlaylists {
73        category_id: &'a str,
74        limit: u8,
75        offset: u32,
76    },
77
78    SavedTracks {
79        limit: u8,
80        offset: u32,
81    },
82    SavedTracksIds {
83        ids: &'a str,
84    },
85    SavedTracksContains {
86        ids: &'a str,
87    },
88
89    SavedAlbums {
90        limit: u8,
91        offset: u32,
92    },
93    SavedAlbumsIds {
94        ids: &'a str,
95    },
96    SavedAlbumsContains {
97        ids: &'a str,
98    },
99
100    Track {
101        id: &'a str,
102    },
103    Tracks {
104        ids: &'a str,
105    },
106
107    Album {
108        id: &'a str,
109    },
110    Albums {
111        ids: &'a str,
112    },
113    AlbumTracks {
114        id: &'a str,
115        limit: u8,
116        offset: u32,
117    },
118    NewReleases {
119        limit: u8,
120        offset: u32,
121    },
122
123    Artist {
124        id: &'a str,
125    },
126    Artists {
127        ids: &'a str,
128    },
129    ArtistTopTracks {
130        id: &'a str,
131        market: &'a str,
132    },
133    ArtistAlbums {
134        id: &'a str,
135        limit: u8,
136        offset: u32,
137    },
138    ArtistRelatedArtists {
139        id: &'a str,
140    },
141
142    CurrentUser,
143    UserProfile {
144        user_id: &'a str,
145    },
146    UserTopItems {
147        item_type: &'a str,
148        time_range: &'a str,
149        limit: u8,
150        offset: u32,
151    },
152    FollowedArtists {
153        limit: u8,
154    },
155    FollowArtistsOrUsers {
156        entity_type: &'a str,
157        ids: &'a str,
158    },
159    FollowingContains {
160        entity_type: &'a str,
161        ids: &'a str,
162    },
163    FollowPlaylist {
164        playlist_id: &'a str,
165    },
166    FollowPlaylistContains {
167        playlist_id: &'a str,
168        ids: &'a str,
169    },
170
171    Category {
172        id: &'a str,
173    },
174    Categories {
175        limit: u8,
176        offset: u32,
177    },
178
179    Markets,
180
181    Search {
182        query: &'a str,
183        types: &'a str,
184        limit: u8,
185        market: Option<&'a str>,
186    },
187
188    Show {
189        id: &'a str,
190    },
191    Shows {
192        ids: &'a str,
193    },
194    ShowEpisodes {
195        id: &'a str,
196        limit: u8,
197        offset: u32,
198    },
199    SavedShows {
200        limit: u8,
201        offset: u32,
202    },
203    SavedShowsIds {
204        ids: &'a str,
205    },
206    SavedShowsContains {
207        ids: &'a str,
208    },
209
210    Episode {
211        id: &'a str,
212    },
213    Episodes {
214        ids: &'a str,
215    },
216    SavedEpisodes {
217        limit: u8,
218        offset: u32,
219    },
220    SavedEpisodesIds {
221        ids: &'a str,
222    },
223    SavedEpisodesContains {
224        ids: &'a str,
225    },
226
227    Audiobook {
228        id: &'a str,
229    },
230    Audiobooks {
231        ids: &'a str,
232    },
233    AudiobookChapters {
234        id: &'a str,
235        limit: u8,
236        offset: u32,
237    },
238    SavedAudiobooks {
239        limit: u8,
240        offset: u32,
241    },
242    SavedAudiobooksIds {
243        ids: &'a str,
244    },
245    SavedAudiobooksContains {
246        ids: &'a str,
247    },
248
249    Chapter {
250        id: &'a str,
251    },
252    Chapters {
253        ids: &'a str,
254    },
255}
256
257impl<'a> Endpoint<'a> {
258    /// Get the path string for this endpoint
259    pub fn path(&self) -> String {
260        match self {
261            Endpoint::PlayerState => "/me/player".to_string(),
262            Endpoint::PlayerCurrentlyPlaying => "/me/player/currently-playing".to_string(),
263            Endpoint::PlayerPlay => "/me/player/play".to_string(),
264            Endpoint::PlayerPause => "/me/player/pause".to_string(),
265            Endpoint::PlayerNext => "/me/player/next".to_string(),
266            Endpoint::PlayerPrevious => "/me/player/previous".to_string(),
267            Endpoint::PlayerQueue => "/me/player/queue".to_string(),
268            Endpoint::PlayerQueueAdd { uri } => format!("/me/player/queue?uri={}", encode(uri)),
269            Endpoint::PlayerDevices => "/me/player/devices".to_string(),
270            Endpoint::PlayerRecentlyPlayed => "/me/player/recently-played".to_string(),
271            Endpoint::PlayerSeek { position_ms } => {
272                format!("/me/player/seek?position_ms={}", position_ms)
273            }
274            Endpoint::PlayerVolume { volume_percent } => {
275                format!("/me/player/volume?volume_percent={}", volume_percent)
276            }
277            Endpoint::PlayerShuffle { state } => format!("/me/player/shuffle?state={}", state),
278            Endpoint::PlayerRepeat { state } => format!("/me/player/repeat?state={}", state),
279
280            Endpoint::Playlist { id } => format!("/playlists/{}", id),
281            Endpoint::PlaylistTracks { id } => format!("/playlists/{}/tracks", id),
282            Endpoint::PlaylistItems { id, limit, offset } => {
283                format!("/playlists/{}/tracks?limit={}&offset={}", id, limit, offset)
284            }
285            Endpoint::PlaylistFollowers { id } => format!("/playlists/{}/followers", id),
286            Endpoint::PlaylistCoverImage { id } => format!("/playlists/{}/images", id),
287            Endpoint::CurrentUserPlaylists { limit, offset } => {
288                format!("/me/playlists?limit={}&offset={}", limit, offset)
289            }
290            Endpoint::UserPlaylists { user_id } => format!("/users/{}/playlists", user_id),
291            Endpoint::CategoryPlaylists {
292                category_id,
293                limit,
294                offset,
295            } => {
296                format!(
297                    "/browse/categories/{}/playlists?limit={}&offset={}",
298                    category_id, limit, offset
299                )
300            }
301
302            Endpoint::SavedTracks { limit, offset } => {
303                format!("/me/tracks?limit={}&offset={}", limit, offset)
304            }
305            Endpoint::SavedTracksIds { ids } => format!("/me/tracks?ids={}", ids),
306            Endpoint::SavedTracksContains { ids } => format!("/me/tracks/contains?ids={}", ids),
307
308            Endpoint::SavedAlbums { limit, offset } => {
309                format!("/me/albums?limit={}&offset={}", limit, offset)
310            }
311            Endpoint::SavedAlbumsIds { ids } => format!("/me/albums?ids={}", ids),
312            Endpoint::SavedAlbumsContains { ids } => format!("/me/albums/contains?ids={}", ids),
313
314            Endpoint::Track { id } => format!("/tracks/{}", id),
315            Endpoint::Tracks { ids } => format!("/tracks?ids={}", ids),
316
317            Endpoint::Album { id } => format!("/albums/{}", id),
318            Endpoint::Albums { ids } => format!("/albums?ids={}", ids),
319            Endpoint::AlbumTracks { id, limit, offset } => {
320                format!("/albums/{}/tracks?limit={}&offset={}", id, limit, offset)
321            }
322            Endpoint::NewReleases { limit, offset } => {
323                format!("/browse/new-releases?limit={}&offset={}", limit, offset)
324            }
325
326            Endpoint::Artist { id } => format!("/artists/{}", id),
327            Endpoint::Artists { ids } => format!("/artists?ids={}", ids),
328            Endpoint::ArtistTopTracks { id, market } => {
329                format!("/artists/{}/top-tracks?market={}", id, market)
330            }
331            Endpoint::ArtistAlbums { id, limit, offset } => {
332                format!("/artists/{}/albums?limit={}&offset={}", id, limit, offset)
333            }
334            Endpoint::ArtistRelatedArtists { id } => format!("/artists/{}/related-artists", id),
335
336            Endpoint::CurrentUser => "/me".to_string(),
337            Endpoint::UserProfile { user_id } => format!("/users/{}", user_id),
338            Endpoint::UserTopItems {
339                item_type,
340                time_range,
341                limit,
342                offset,
343            } => {
344                format!(
345                    "/me/top/{}?time_range={}&limit={}&offset={}",
346                    item_type, time_range, limit, offset
347                )
348            }
349            Endpoint::FollowedArtists { limit } => {
350                format!("/me/following?type=artist&limit={}", limit)
351            }
352            Endpoint::FollowArtistsOrUsers { entity_type, ids } => {
353                format!("/me/following?type={}&ids={}", entity_type, ids)
354            }
355            Endpoint::FollowingContains { entity_type, ids } => {
356                format!("/me/following/contains?type={}&ids={}", entity_type, ids)
357            }
358            Endpoint::FollowPlaylist { playlist_id } => {
359                format!("/playlists/{}/followers", playlist_id)
360            }
361            Endpoint::FollowPlaylistContains { playlist_id, ids } => {
362                format!("/playlists/{}/followers/contains?ids={}", playlist_id, ids)
363            }
364
365            Endpoint::Category { id } => format!("/browse/categories/{}", id),
366            Endpoint::Categories { limit, offset } => {
367                format!("/browse/categories?limit={}&offset={}", limit, offset)
368            }
369
370            Endpoint::Markets => "/markets".to_string(),
371
372            Endpoint::Search {
373                query,
374                types,
375                limit,
376                market,
377            } => {
378                let base = format!("/search?q={}&type={}&limit={}", encode(query), types, limit);
379                match market {
380                    Some(m) => format!("{}&market={}", base, m),
381                    None => base,
382                }
383            }
384
385            Endpoint::Show { id } => format!("/shows/{}", id),
386            Endpoint::Shows { ids } => format!("/shows?ids={}", ids),
387            Endpoint::ShowEpisodes { id, limit, offset } => {
388                format!("/shows/{}/episodes?limit={}&offset={}", id, limit, offset)
389            }
390            Endpoint::SavedShows { limit, offset } => {
391                format!("/me/shows?limit={}&offset={}", limit, offset)
392            }
393            Endpoint::SavedShowsIds { ids } => format!("/me/shows?ids={}", ids),
394            Endpoint::SavedShowsContains { ids } => format!("/me/shows/contains?ids={}", ids),
395
396            Endpoint::Episode { id } => format!("/episodes/{}", id),
397            Endpoint::Episodes { ids } => format!("/episodes?ids={}", ids),
398            Endpoint::SavedEpisodes { limit, offset } => {
399                format!("/me/episodes?limit={}&offset={}", limit, offset)
400            }
401            Endpoint::SavedEpisodesIds { ids } => format!("/me/episodes?ids={}", ids),
402            Endpoint::SavedEpisodesContains { ids } => format!("/me/episodes/contains?ids={}", ids),
403
404            Endpoint::Audiobook { id } => format!("/audiobooks/{}", id),
405            Endpoint::Audiobooks { ids } => format!("/audiobooks?ids={}", ids),
406            Endpoint::AudiobookChapters { id, limit, offset } => {
407                format!(
408                    "/audiobooks/{}/chapters?limit={}&offset={}",
409                    id, limit, offset
410                )
411            }
412            Endpoint::SavedAudiobooks { limit, offset } => {
413                format!("/me/audiobooks?limit={}&offset={}", limit, offset)
414            }
415            Endpoint::SavedAudiobooksIds { ids } => format!("/me/audiobooks?ids={}", ids),
416            Endpoint::SavedAudiobooksContains { ids } => {
417                format!("/me/audiobooks/contains?ids={}", ids)
418            }
419
420            Endpoint::Chapter { id } => format!("/chapters/{}", id),
421            Endpoint::Chapters { ids } => format!("/chapters?ids={}", ids),
422        }
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn player_endpoints() {
432        assert_eq!(Endpoint::PlayerState.path(), "/me/player");
433        assert_eq!(
434            Endpoint::PlayerCurrentlyPlaying.path(),
435            "/me/player/currently-playing"
436        );
437        assert_eq!(Endpoint::PlayerPlay.path(), "/me/player/play");
438        assert_eq!(Endpoint::PlayerPause.path(), "/me/player/pause");
439        assert_eq!(Endpoint::PlayerNext.path(), "/me/player/next");
440        assert_eq!(Endpoint::PlayerPrevious.path(), "/me/player/previous");
441        assert_eq!(Endpoint::PlayerQueue.path(), "/me/player/queue");
442        assert_eq!(Endpoint::PlayerDevices.path(), "/me/player/devices");
443        assert_eq!(
444            Endpoint::PlayerRecentlyPlayed.path(),
445            "/me/player/recently-played"
446        );
447    }
448
449    #[test]
450    fn player_endpoints_with_params() {
451        assert_eq!(
452            Endpoint::PlayerSeek { position_ms: 30000 }.path(),
453            "/me/player/seek?position_ms=30000"
454        );
455        assert_eq!(
456            Endpoint::PlayerVolume { volume_percent: 50 }.path(),
457            "/me/player/volume?volume_percent=50"
458        );
459        assert_eq!(
460            Endpoint::PlayerShuffle { state: true }.path(),
461            "/me/player/shuffle?state=true"
462        );
463        assert_eq!(
464            Endpoint::PlayerRepeat { state: "track" }.path(),
465            "/me/player/repeat?state=track"
466        );
467    }
468
469    #[test]
470    fn queue_add_encodes_uri() {
471        let uri = "spotify:track:abc123";
472        let path = Endpoint::PlayerQueueAdd { uri }.path();
473        assert!(path.starts_with("/me/player/queue?uri="));
474        assert!(path.contains("spotify"));
475    }
476
477    #[test]
478    fn playlist_endpoints() {
479        assert_eq!(
480            Endpoint::Playlist { id: "abc123" }.path(),
481            "/playlists/abc123"
482        );
483        assert_eq!(
484            Endpoint::PlaylistTracks { id: "abc123" }.path(),
485            "/playlists/abc123/tracks"
486        );
487        assert_eq!(
488            Endpoint::PlaylistItems {
489                id: "abc123",
490                limit: 50,
491                offset: 0
492            }
493            .path(),
494            "/playlists/abc123/tracks?limit=50&offset=0"
495        );
496        assert_eq!(
497            Endpoint::PlaylistFollowers { id: "abc123" }.path(),
498            "/playlists/abc123/followers"
499        );
500    }
501
502    #[test]
503    fn user_playlist_endpoints() {
504        assert_eq!(
505            Endpoint::CurrentUserPlaylists {
506                limit: 20,
507                offset: 40
508            }
509            .path(),
510            "/me/playlists?limit=20&offset=40"
511        );
512        assert_eq!(
513            Endpoint::UserPlaylists { user_id: "user123" }.path(),
514            "/users/user123/playlists"
515        );
516    }
517
518    #[test]
519    fn browse_endpoints() {
520        assert_eq!(
521            Endpoint::CategoryPlaylists {
522                category_id: "pop",
523                limit: 20,
524                offset: 0
525            }
526            .path(),
527            "/browse/categories/pop/playlists?limit=20&offset=0"
528        );
529        assert_eq!(
530            Endpoint::NewReleases {
531                limit: 20,
532                offset: 0
533            }
534            .path(),
535            "/browse/new-releases?limit=20&offset=0"
536        );
537        assert_eq!(
538            Endpoint::Category { id: "rock" }.path(),
539            "/browse/categories/rock"
540        );
541        assert_eq!(
542            Endpoint::Categories {
543                limit: 50,
544                offset: 0
545            }
546            .path(),
547            "/browse/categories?limit=50&offset=0"
548        );
549    }
550
551    #[test]
552    fn saved_tracks_endpoints() {
553        assert_eq!(
554            Endpoint::SavedTracks {
555                limit: 20,
556                offset: 0
557            }
558            .path(),
559            "/me/tracks?limit=20&offset=0"
560        );
561        assert_eq!(
562            Endpoint::SavedTracksIds { ids: "id1,id2" }.path(),
563            "/me/tracks?ids=id1,id2"
564        );
565        assert_eq!(
566            Endpoint::SavedTracksContains { ids: "id1" }.path(),
567            "/me/tracks/contains?ids=id1"
568        );
569    }
570
571    #[test]
572    fn saved_albums_endpoints() {
573        assert_eq!(
574            Endpoint::SavedAlbums {
575                limit: 20,
576                offset: 0
577            }
578            .path(),
579            "/me/albums?limit=20&offset=0"
580        );
581        assert_eq!(
582            Endpoint::SavedAlbumsIds { ids: "id1,id2" }.path(),
583            "/me/albums?ids=id1,id2"
584        );
585        assert_eq!(
586            Endpoint::SavedAlbumsContains { ids: "id1" }.path(),
587            "/me/albums/contains?ids=id1"
588        );
589    }
590
591    #[test]
592    fn track_endpoints() {
593        assert_eq!(
594            Endpoint::Track { id: "track123" }.path(),
595            "/tracks/track123"
596        );
597        assert_eq!(
598            Endpoint::Tracks { ids: "t1,t2,t3" }.path(),
599            "/tracks?ids=t1,t2,t3"
600        );
601    }
602
603    #[test]
604    fn album_endpoints() {
605        assert_eq!(
606            Endpoint::Album { id: "album123" }.path(),
607            "/albums/album123"
608        );
609        assert_eq!(
610            Endpoint::Albums { ids: "a1,a2" }.path(),
611            "/albums?ids=a1,a2"
612        );
613        assert_eq!(
614            Endpoint::AlbumTracks {
615                id: "album123",
616                limit: 50,
617                offset: 0
618            }
619            .path(),
620            "/albums/album123/tracks?limit=50&offset=0"
621        );
622    }
623
624    #[test]
625    fn artist_endpoints() {
626        assert_eq!(
627            Endpoint::Artist { id: "artist123" }.path(),
628            "/artists/artist123"
629        );
630        assert_eq!(
631            Endpoint::Artists { ids: "a1,a2" }.path(),
632            "/artists?ids=a1,a2"
633        );
634        assert_eq!(
635            Endpoint::ArtistTopTracks {
636                id: "artist123",
637                market: "US"
638            }
639            .path(),
640            "/artists/artist123/top-tracks?market=US"
641        );
642        assert_eq!(
643            Endpoint::ArtistAlbums {
644                id: "artist123",
645                limit: 20,
646                offset: 0
647            }
648            .path(),
649            "/artists/artist123/albums?limit=20&offset=0"
650        );
651        assert_eq!(
652            Endpoint::ArtistRelatedArtists { id: "artist123" }.path(),
653            "/artists/artist123/related-artists"
654        );
655    }
656
657    #[test]
658    fn user_endpoints() {
659        assert_eq!(Endpoint::CurrentUser.path(), "/me");
660        assert_eq!(
661            Endpoint::UserProfile { user_id: "user123" }.path(),
662            "/users/user123"
663        );
664        assert_eq!(
665            Endpoint::UserTopItems {
666                item_type: "tracks",
667                time_range: "medium_term",
668                limit: 20,
669                offset: 0
670            }
671            .path(),
672            "/me/top/tracks?time_range=medium_term&limit=20&offset=0"
673        );
674    }
675
676    #[test]
677    fn follow_endpoints() {
678        assert_eq!(
679            Endpoint::FollowedArtists { limit: 20 }.path(),
680            "/me/following?type=artist&limit=20"
681        );
682        assert_eq!(
683            Endpoint::FollowArtistsOrUsers {
684                entity_type: "artist",
685                ids: "id1,id2"
686            }
687            .path(),
688            "/me/following?type=artist&ids=id1,id2"
689        );
690        assert_eq!(
691            Endpoint::FollowingContains {
692                entity_type: "artist",
693                ids: "id1"
694            }
695            .path(),
696            "/me/following/contains?type=artist&ids=id1"
697        );
698        assert_eq!(
699            Endpoint::FollowPlaylist {
700                playlist_id: "pl123"
701            }
702            .path(),
703            "/playlists/pl123/followers"
704        );
705        assert_eq!(
706            Endpoint::FollowPlaylistContains {
707                playlist_id: "pl123",
708                ids: "user1"
709            }
710            .path(),
711            "/playlists/pl123/followers/contains?ids=user1"
712        );
713    }
714
715    #[test]
716    fn markets_endpoint() {
717        assert_eq!(Endpoint::Markets.path(), "/markets");
718    }
719
720    #[test]
721    fn search_encodes_query() {
722        let path = Endpoint::Search {
723            query: "hello world",
724            types: "track",
725            limit: 20,
726            market: None,
727        }
728        .path();
729        assert!(path.contains("hello%20world") || path.contains("hello+world"));
730        assert!(path.contains("type=track"));
731        assert!(path.contains("limit=20"));
732    }
733
734    #[test]
735    fn search_includes_market() {
736        let path = Endpoint::Search {
737            query: "test",
738            types: "track",
739            limit: 20,
740            market: Some("US"),
741        }
742        .path();
743        assert!(path.contains("market=US"));
744    }
745
746    #[test]
747    fn show_endpoints() {
748        assert_eq!(Endpoint::Show { id: "show123" }.path(), "/shows/show123");
749        assert_eq!(Endpoint::Shows { ids: "s1,s2" }.path(), "/shows?ids=s1,s2");
750        assert_eq!(
751            Endpoint::ShowEpisodes {
752                id: "show123",
753                limit: 20,
754                offset: 0
755            }
756            .path(),
757            "/shows/show123/episodes?limit=20&offset=0"
758        );
759        assert_eq!(
760            Endpoint::SavedShows {
761                limit: 20,
762                offset: 0
763            }
764            .path(),
765            "/me/shows?limit=20&offset=0"
766        );
767    }
768
769    #[test]
770    fn episode_endpoints() {
771        assert_eq!(Endpoint::Episode { id: "ep123" }.path(), "/episodes/ep123");
772        assert_eq!(
773            Endpoint::Episodes { ids: "e1,e2" }.path(),
774            "/episodes?ids=e1,e2"
775        );
776        assert_eq!(
777            Endpoint::SavedEpisodes {
778                limit: 20,
779                offset: 0
780            }
781            .path(),
782            "/me/episodes?limit=20&offset=0"
783        );
784    }
785
786    #[test]
787    fn audiobook_endpoints() {
788        assert_eq!(
789            Endpoint::Audiobook { id: "ab123" }.path(),
790            "/audiobooks/ab123"
791        );
792        assert_eq!(
793            Endpoint::Audiobooks { ids: "ab1,ab2" }.path(),
794            "/audiobooks?ids=ab1,ab2"
795        );
796        assert_eq!(
797            Endpoint::AudiobookChapters {
798                id: "ab123",
799                limit: 50,
800                offset: 0
801            }
802            .path(),
803            "/audiobooks/ab123/chapters?limit=50&offset=0"
804        );
805        assert_eq!(
806            Endpoint::SavedAudiobooks {
807                limit: 20,
808                offset: 0
809            }
810            .path(),
811            "/me/audiobooks?limit=20&offset=0"
812        );
813    }
814
815    #[test]
816    fn chapter_endpoints() {
817        assert_eq!(Endpoint::Chapter { id: "ch123" }.path(), "/chapters/ch123");
818        assert_eq!(
819            Endpoint::Chapters { ids: "c1,c2" }.path(),
820            "/chapters?ids=c1,c2"
821        );
822    }
823
824    #[test]
825    fn saved_shows_ids_endpoint() {
826        assert_eq!(
827            Endpoint::SavedShowsIds { ids: "show1,show2" }.path(),
828            "/me/shows?ids=show1,show2"
829        );
830    }
831
832    #[test]
833    fn saved_shows_contains_endpoint() {
834        assert_eq!(
835            Endpoint::SavedShowsContains { ids: "show1" }.path(),
836            "/me/shows/contains?ids=show1"
837        );
838    }
839
840    #[test]
841    fn saved_episodes_ids_endpoint() {
842        assert_eq!(
843            Endpoint::SavedEpisodesIds { ids: "ep1,ep2" }.path(),
844            "/me/episodes?ids=ep1,ep2"
845        );
846    }
847
848    #[test]
849    fn saved_episodes_contains_endpoint() {
850        assert_eq!(
851            Endpoint::SavedEpisodesContains { ids: "ep1" }.path(),
852            "/me/episodes/contains?ids=ep1"
853        );
854    }
855
856    #[test]
857    fn saved_audiobooks_ids_endpoint() {
858        assert_eq!(
859            Endpoint::SavedAudiobooksIds { ids: "ab1,ab2" }.path(),
860            "/me/audiobooks?ids=ab1,ab2"
861        );
862    }
863
864    #[test]
865    fn saved_audiobooks_contains_endpoint() {
866        assert_eq!(
867            Endpoint::SavedAudiobooksContains { ids: "ab1" }.path(),
868            "/me/audiobooks/contains?ids=ab1"
869        );
870    }
871
872    #[test]
873    fn playlist_cover_image_endpoint() {
874        assert_eq!(
875            Endpoint::PlaylistCoverImage { id: "pl123" }.path(),
876            "/playlists/pl123/images"
877        );
878    }
879}