rspotify_model/
idtypes.rs

1//! This module makes it possible to represent Spotify IDs and URIs with type
2//! safety and almost no overhead.
3//!
4//! ## Concrete IDs
5//!
6//! The trait [`Id`] is the central element of this module. It's implemented by
7//! all kinds of ID, and includes the main functionality to use them. Remember
8//! that you will need to import this trait to access its methods. The easiest
9//! way is to add `use rspotify::prelude::*`.
10//!
11//! * [`Type::Artist`] => [`ArtistId`]
12//! * [`Type::Album`] => [`AlbumId`]
13//! * [`Type::Track`] => [`TrackId`]
14//! * [`Type::Playlist`] => [`PlaylistId`]
15//! * [`Type::User`] => [`UserId`]
16//! * [`Type::Show`] => [`ShowId`]
17//! * [`Type::Episode`] => [`EpisodeId`]
18//!
19//! Every kind of ID defines its own validity function, i.e., what characters it
20//! can be made up of, such as alphanumeric or any.
21//!
22//! These types are just wrappers for [`Cow<str>`], so their usage should be
23//! quite similar overall.
24//!
25//! [`Cow<str>`]: [`std::borrow::Cow`]
26//!
27//! ## Examples
28//!
29//! If an endpoint requires a `TrackId`, you may pass it as:
30//!
31//! ```
32//! # use rspotify_model::TrackId;
33//! fn pause_track(id: TrackId<'_>) { /* ... */ }
34//!
35//! let id = TrackId::from_id("4iV5W9uYEdYUVa79Axb7Rh").unwrap();
36//! pause_track(id);
37//! ```
38//!
39//! Notice how this way it's type safe; the following example would fail at
40//! compile-time:
41//!
42//! ```compile_fail
43//! # use rspotify_model::{TrackId, EpisodeId};
44//! fn pause_track(id: TrackId<'_>) { /* ... */ }
45//!
46//! let id = EpisodeId::from_id("4iV5W9uYEdYUVa79Axb7Rh").unwrap();
47//! pause_track(id);
48//! ```
49//!
50//! And this would panic because it's a `TrackId` but its URI string specifies
51//! it's an album (`spotify:album:xxxx`).
52//!
53//! ```should_panic
54//! # use rspotify_model::TrackId;
55//! fn pause_track(id: TrackId<'_>) { /* ... */ }
56//!
57//! let id = TrackId::from_uri("spotify:album:6akEvsycLGftJxYudPjmqK").unwrap();
58//! pause_track(id);
59//! ```
60//!
61//! A more complex example where an endpoint takes a vector of IDs of different
62//! types:
63//!
64//! ```
65//! use rspotify_model::{TrackId, EpisodeId, PlayableId};
66//!
67//! fn track(id: TrackId<'_>) { /* ... */ }
68//! fn episode(id: EpisodeId<'_>) { /* ... */ }
69//! fn add_to_queue(id: &[PlayableId<'_>]) { /* ... */ }
70//!
71//! let tracks = [
72//!     TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(),
73//!     TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap(),
74//! ];
75//! let episodes = [
76//!     EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap(),
77//!     EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap(),
78//! ];
79//!
80//! // First we get some info about the tracks and episodes
81//! let track_info = tracks.iter().map(|id| track(id.as_ref())).collect::<Vec<_>>();
82//! let ep_info = episodes.iter().map(|id| episode(id.as_ref())).collect::<Vec<_>>();
83//! println!("Track info: {:?}", track_info);
84//! println!("Episode info: {:?}", ep_info);
85//!
86//! // And then we add both the tracks and episodes to the queue
87//! let playable = tracks
88//!     .into_iter()
89//!     .map(|t| t.as_ref().into())
90//!     .chain(
91//!         episodes.into_iter().map(|e| e.as_ref().into())
92//!     )
93//!     .collect::<Vec<PlayableId>>();
94//! add_to_queue(&playable);
95//! ```
96
97use enum_dispatch::enum_dispatch;
98use serde::{Deserialize, Serialize};
99use strum::Display;
100use thiserror::Error;
101
102use std::{borrow::Cow, fmt::Debug, hash::Hash};
103
104use crate::Type;
105
106/// Spotify ID or URI parsing error
107///
108/// See also [`Id`] for details.
109#[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)]
110pub enum IdError {
111    /// Spotify URI prefix is not `spotify:` or `spotify/`.
112    InvalidPrefix,
113    /// Spotify URI can't be split into type and id parts (e.g., it has invalid
114    /// separator).
115    InvalidFormat,
116    /// Spotify URI has invalid type name, or id has invalid type in a given
117    /// context (e.g. a method expects a track id, but artist id is provided).
118    InvalidType,
119    /// Spotify id is invalid (empty or contains invalid characters).
120    InvalidId,
121}
122
123/// The main interface for an ID.
124///
125/// See the [module level documentation] for more information.
126///
127/// [module level documentation]: [`crate::idtypes`]
128#[enum_dispatch]
129pub trait Id {
130    /// Returns the inner Spotify object ID, which is guaranteed to be valid for
131    /// its type.
132    fn id(&self) -> &str;
133
134    /// The type of the ID, as a function.
135    fn _type(&self) -> Type;
136
137    /// Returns a Spotify object URI in a well-known format: `spotify:type:id`.
138    ///
139    /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`,
140    /// `spotify:track:4y4VO05kYgUTo2bzbox1an`.
141    fn uri(&self) -> String {
142        format!("spotify:{}:{}", self._type(), self.id())
143    }
144
145    /// Returns a full Spotify object URL that can be opened in a browser.
146    ///
147    /// Examples: `https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an`,
148    /// `https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI`.
149    fn url(&self) -> String {
150        format!("https://open.spotify.com/{}/{}", self._type(), self.id())
151    }
152}
153
154/// A lower level function to parse a URI into both its type and its actual ID.
155/// Note that this function doesn't check the validity of the returned ID (e.g.,
156/// whether it's alphanumeric; that should be done in `Id::from_id`).
157///
158/// This is only useful for advanced use-cases, such as implementing your own ID
159/// type.
160pub fn parse_uri(uri: &str) -> Result<(Type, &str), IdError> {
161    let mut chars = uri
162        .strip_prefix("spotify")
163        .ok_or(IdError::InvalidPrefix)?
164        .chars();
165    let sep = match chars.next() {
166        Some(ch) if ch == '/' || ch == ':' => ch,
167        _ => return Err(IdError::InvalidPrefix),
168    };
169    let rest = chars.as_str();
170
171    let (tpe, id) = rest
172        .rfind(sep)
173        .map(|mid| rest.split_at(mid))
174        .ok_or(IdError::InvalidFormat)?;
175
176    // Note that in case the type isn't known at compile time,
177    // any type will be accepted.
178    match tpe.parse::<Type>() {
179        Ok(tpe) => Ok((tpe, &id[1..])),
180        _ => Err(IdError::InvalidType),
181    }
182}
183
184/// This macro helps consistently define ID types.
185///
186/// * The `$type` parameter indicates what variant in `Type` the ID is for (say,
187///   `Artist`, or `Album`).
188/// * The `$name` parameter is the identifier of the struct.
189/// * The `$validity` parameter is the implementation of `id_is_valid`.
190macro_rules! define_idtypes {
191    ($($type:ident => {
192        name: $name:ident,
193        validity: $validity:expr
194    }),+) => {
195        $(
196            #[doc = concat!(
197                "ID of type [`Type::", stringify!($type), "`]. The validity of \
198                its characters is defined by the closure `",
199                stringify!($validity), "`.\n\nRefer to the [module-level \
200                docs][`crate::idtypes`] for more information. "
201            )]
202            #[repr(transparent)]
203            #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
204            pub struct $name<'a>(Cow<'a, str>);
205
206            impl<'a> $name<'a> {
207                /// The type of the ID, as a constant.
208                const TYPE: Type = Type::$type;
209
210                /// Only returns `true` in case the given string is valid
211                /// according to that specific ID (e.g., some may require
212                /// alphanumeric characters only).
213                #[must_use]
214                pub fn id_is_valid(id: &str) -> bool {
215                    const VALID_FN: fn(&str) -> bool = $validity;
216                    VALID_FN(id)
217                }
218
219                /// Initialize the ID without checking its validity.
220                ///
221                /// # Safety
222                ///
223                /// The string passed to this method must be made out of valid
224                /// characters only; otherwise undefined behaviour may occur.
225                pub unsafe fn from_id_unchecked<S>(id: S) -> Self
226                    where
227                        S: Into<Cow<'a, str>>
228                {
229                    Self(id.into())
230                }
231
232                /// Parse Spotify ID from string slice.
233                ///
234                /// A valid Spotify object id must be a non-empty string with
235                /// valid characters.
236                ///
237                /// # Errors
238                ///
239                /// - `IdError::InvalidId` - if `id` contains invalid characters.
240                pub fn from_id<S>(id: S) -> Result<Self, IdError>
241                    where
242                        S: Into<Cow<'a, str>>
243                {
244                    let id = id.into();
245                    if Self::id_is_valid(&id) {
246                        // Safe, we've just checked that the ID is valid.
247                        Ok(unsafe { Self::from_id_unchecked(id) })
248                    } else {
249                        Err(IdError::InvalidId)
250                    }
251                }
252
253                /// Parse Spotify URI from string slice
254                ///
255                /// Spotify URI must be in one of the following formats:
256                /// `spotify:{type}:{id}` or `spotify/{type}/{id}`.
257                /// Where `{type}` is one of `artist`, `album`, `track`,
258                /// `playlist`, `user`, `show`, or `episode`, and `{id}` is a
259                /// non-empty valid string.
260                ///
261                /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`,
262                /// `spotify/track/4y4VO05kYgUTo2bzbox1an`.
263                ///
264                /// # Errors
265                ///
266                /// - `IdError::InvalidPrefix` - if `uri` is not started with
267                ///   `spotify:` or `spotify/`,
268                /// - `IdError::InvalidType` - if type part of an `uri` is not a
269                ///   valid Spotify type `T`,
270                /// - `IdError::InvalidId` - if id part of an `uri` is not a
271                ///   valid id,
272                /// - `IdError::InvalidFormat` - if it can't be splitted into
273                ///   type and id parts.
274                ///
275                /// # Implementation details
276                ///
277                /// Unlike [`Self::from_id`], this method takes a `&str` rather
278                /// than an `Into<Cow<str>>`. This is because the inner `Cow` in
279                /// the ID would reference a slice from the given `&str` (i.e.,
280                /// taking the ID out of the URI). The parameter wouldn't live
281                /// long enough when using `Into<Cow<str>>`, so the only
282                /// sensible choice is to just use a `&str`.
283                pub fn from_uri(uri: &'a str) -> Result<Self, IdError> {
284                    let (tpe, id) = parse_uri(&uri)?;
285                    if tpe == Type::$type {
286                        Self::from_id(id)
287                    } else {
288                        Err(IdError::InvalidType)
289                    }
290                }
291
292                /// Parse Spotify ID or URI from string slice
293                ///
294                /// Spotify URI must be in one of the following formats:
295                /// `spotify:{type}:{id}` or `spotify/{type}/{id}`.
296                /// Where `{type}` is one of `artist`, `album`, `track`,
297                /// `playlist`, `user`, `show`, or `episode`, and `{id}` is a
298                /// non-empty valid string. The URI must be match with the ID's
299                /// type (`Id::TYPE`), otherwise `IdError::InvalidType` error is
300                /// returned.
301                ///
302                /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`,
303                /// `spotify/track/4y4VO05kYgUTo2bzbox1an`.
304                ///
305                /// If input string is not a valid Spotify URI (it's not started
306                /// with `spotify:` or `spotify/`), it must be a valid Spotify
307                /// object ID, i.e. a non-empty valid string.
308                ///
309                /// # Errors
310                ///
311                /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and
312                ///   it's type part is not equal to `T`,
313                /// - `IdError::InvalidId` - either if `id_or_uri` is an URI
314                ///   with invalid id part, or it's an invalid id (id is invalid
315                ///   if it contains valid characters),
316                /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and
317                ///   it can't be split into type and id parts.
318                ///
319                /// # Implementation details
320                ///
321                /// Unlike [`Self::from_id`], this method takes a `&str` rather
322                /// than an `Into<Cow<str>>`. This is because the inner `Cow` in
323                /// the ID would reference a slice from the given `&str` (i.e.,
324                /// taking the ID out of the URI). The parameter wouldn't live
325                /// long enough when using `Into<Cow<str>>`, so the only
326                /// sensible choice is to just use a `&str`.
327                pub fn from_id_or_uri(id_or_uri: &'a str) -> Result<Self, IdError> {
328                    match Self::from_uri(id_or_uri) {
329                        Ok(id) => Ok(id),
330                        Err(IdError::InvalidPrefix) => Self::from_id(id_or_uri),
331                        Err(error) => Err(error),
332                    }
333                }
334
335                /// This creates an ID with the underlying `&str` variant from a
336                /// reference. Useful to use an ID multiple times without having
337                /// to clone it.
338                #[must_use]
339                pub fn as_ref(&'a self) -> Self {
340                    Self(Cow::Borrowed(self.0.as_ref()))
341                }
342
343                /// An ID is a `Cow` after all, so this will switch to the its
344                /// owned version, which has a `'static` lifetime.
345                #[must_use]
346                pub fn into_static(self) -> $name<'static> {
347                    $name(Cow::Owned(self.0.into_owned()))
348                }
349
350                /// Similar to [`Self::into_static`], but without consuming the
351                /// original ID.
352                #[must_use]
353                pub fn clone_static(&self) -> $name<'static> {
354                    $name(Cow::Owned(self.0.clone().into_owned()))
355                }
356            }
357
358            impl Id for $name<'_> {
359                fn id(&self) -> &str {
360                    &self.0
361                }
362
363                fn _type(&self) -> Type {
364                    Self::TYPE
365                }
366            }
367
368            // Deserialization may take either an ID or an URI, so its
369            // implementation has to be done manually.
370            impl<'de> Deserialize<'de> for $name<'static> {
371                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
372                where
373                    D: serde::Deserializer<'de>,
374                {
375                    struct IdVisitor;
376
377                    impl<'de> serde::de::Visitor<'de> for IdVisitor {
378                        type Value = $name<'static>;
379
380                        fn expecting(
381                            &self, formatter: &mut std::fmt::Formatter<'_>
382                        ) -> Result<(), std::fmt::Error>
383                        {
384                            let msg = concat!("ID or URI for struct ", stringify!($name));
385                            formatter.write_str(msg)
386                        }
387
388                        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
389                        where
390                            E: serde::de::Error,
391                        {
392                            $name::from_id_or_uri(value)
393                                .map($name::into_static)
394                                .map_err(serde::de::Error::custom)
395                        }
396
397                        fn visit_newtype_struct<A>(
398                            self,
399                            deserializer: A,
400                        ) -> Result<Self::Value, A::Error>
401                        where
402                            A: serde::Deserializer<'de>,
403                        {
404                            deserializer.deserialize_str(self)
405                        }
406
407                        fn visit_seq<A>(
408                            self,
409                            mut seq: A,
410                        ) -> Result<Self::Value, A::Error>
411                        where
412                            A: serde::de::SeqAccess<'de>,
413                        {
414                            let field: &str = seq.next_element()?
415                                .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
416                            $name::from_id_or_uri(field)
417                                .map($name::into_static)
418                                .map_err(serde::de::Error::custom)
419                        }
420                    }
421
422                    deserializer.deserialize_newtype_struct(stringify!($name), IdVisitor)
423                }
424            }
425
426            /// `Id`s may be borrowed as `str` the same way `Box<T>` may be
427            /// borrowed as `T` or `String` as `str`
428            impl std::borrow::Borrow<str> for $name<'_> {
429                fn borrow(&self) -> &str {
430                    self.id()
431                }
432            }
433
434            /// Displaying the ID shows its URI
435            impl std::fmt::Display for $name<'_> {
436                fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
437                    write!(f, "{}", self.uri())
438                }
439            }
440        )+
441    }
442}
443
444// First declaring the regular IDs. Those with custom behaviour will have to be
445// declared manually later on.
446define_idtypes!(
447    Artist => {
448        name: ArtistId,
449        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
450    },
451    Album => {
452        name: AlbumId,
453        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
454    },
455    Track => {
456        name: TrackId,
457        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
458    },
459    Playlist => {
460        name: PlaylistId,
461        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
462    },
463    Show => {
464        name: ShowId,
465        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
466    },
467    Episode => {
468        name: EpisodeId,
469        validity: |id| id.chars().all(|ch| ch.is_ascii_alphanumeric())
470    },
471    User => {
472        name: UserId,
473        validity: |_| true
474    }
475);
476
477// We use `enum_dispatch` for dynamic dispatch, which is not only easier to use
478// than `dyn`, but also more efficient.
479/// Grouping up multiple kinds of IDs to treat them generically. This also
480/// implements [`Id`], and [`From`] to instantiate it.
481#[enum_dispatch(Id)]
482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
483pub enum PlayContextId<'a> {
484    Artist(ArtistId<'a>),
485    Album(AlbumId<'a>),
486    Playlist(PlaylistId<'a>),
487    Show(ShowId<'a>),
488}
489// These don't work with `enum_dispatch`, unfortunately.
490impl<'a> PlayContextId<'a> {
491    #[must_use]
492    pub fn as_ref(&'a self) -> Self {
493        match self {
494            PlayContextId::Artist(x) => PlayContextId::Artist(x.as_ref()),
495            PlayContextId::Album(x) => PlayContextId::Album(x.as_ref()),
496            PlayContextId::Playlist(x) => PlayContextId::Playlist(x.as_ref()),
497            PlayContextId::Show(x) => PlayContextId::Show(x.as_ref()),
498        }
499    }
500
501    #[must_use]
502    pub fn into_static(self) -> PlayContextId<'static> {
503        match self {
504            PlayContextId::Artist(x) => PlayContextId::Artist(x.into_static()),
505            PlayContextId::Album(x) => PlayContextId::Album(x.into_static()),
506            PlayContextId::Playlist(x) => PlayContextId::Playlist(x.into_static()),
507            PlayContextId::Show(x) => PlayContextId::Show(x.into_static()),
508        }
509    }
510
511    #[must_use]
512    pub fn clone_static(&'a self) -> PlayContextId<'static> {
513        match self {
514            PlayContextId::Artist(x) => PlayContextId::Artist(x.clone_static()),
515            PlayContextId::Album(x) => PlayContextId::Album(x.clone_static()),
516            PlayContextId::Playlist(x) => PlayContextId::Playlist(x.clone_static()),
517            PlayContextId::Show(x) => PlayContextId::Show(x.clone_static()),
518        }
519    }
520}
521
522/// Grouping up multiple kinds of IDs to treat them generically. This also
523/// implements [`Id`] and [`From`] to instantiate it.
524#[enum_dispatch(Id)]
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
526pub enum PlayableId<'a> {
527    Track(TrackId<'a>),
528    Episode(EpisodeId<'a>),
529}
530// These don't work with `enum_dispatch`, unfortunately.
531impl<'a> PlayableId<'a> {
532    #[must_use]
533    pub fn as_ref(&'a self) -> Self {
534        match self {
535            PlayableId::Track(x) => PlayableId::Track(x.as_ref()),
536            PlayableId::Episode(x) => PlayableId::Episode(x.as_ref()),
537        }
538    }
539
540    #[must_use]
541    pub fn into_static(self) -> PlayableId<'static> {
542        match self {
543            PlayableId::Track(x) => PlayableId::Track(x.into_static()),
544            PlayableId::Episode(x) => PlayableId::Episode(x.into_static()),
545        }
546    }
547
548    #[must_use]
549    pub fn clone_static(&'a self) -> PlayableId<'static> {
550        match self {
551            PlayableId::Track(x) => PlayableId::Track(x.clone_static()),
552            PlayableId::Episode(x) => PlayableId::Episode(x.clone_static()),
553        }
554    }
555}
556
557#[cfg(test)]
558mod test {
559    use super::*;
560    use std::{borrow::Cow, error::Error};
561
562    // Valid values:
563    const ID: &str = "4iV5W9uYEdYUVa79Axb7Rh";
564    const URI: &str = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh";
565    const URI_SLASHES: &str = "spotify/track/4iV5W9uYEdYUVa79Axb7Rh";
566    // Invalid values:
567    const URI_EMPTY: &str = "spotify::4iV5W9uYEdYUVa79Axb7Rh";
568    const URI_WRONGTYPE1: &str = "spotify:unknown:4iV5W9uYEdYUVa79Axb7Rh";
569    const URI_SHORT: &str = "track:4iV5W9uYEdYUVa79Axb7Rh";
570    const URI_MIXED1: &str = "spotify/track:4iV5W9uYEdYUVa79Axb7Rh";
571    const URI_MIXED2: &str = "spotify:track/4iV5W9uYEdYUVa79Axb7Rh";
572
573    #[test]
574    fn test_id_parse() {
575        assert!(TrackId::from_id(ID).is_ok());
576        assert_eq!(TrackId::from_id(URI), Err(IdError::InvalidId));
577        assert_eq!(TrackId::from_id(URI_SLASHES), Err(IdError::InvalidId));
578        assert_eq!(TrackId::from_id(URI_EMPTY), Err(IdError::InvalidId));
579        assert_eq!(TrackId::from_id(URI_WRONGTYPE1), Err(IdError::InvalidId));
580        assert_eq!(TrackId::from_id(URI_SHORT), Err(IdError::InvalidId));
581        assert_eq!(TrackId::from_id(URI_MIXED1), Err(IdError::InvalidId));
582        assert_eq!(TrackId::from_id(URI_MIXED2), Err(IdError::InvalidId));
583    }
584
585    #[test]
586    fn test_uri_parse() {
587        assert!(TrackId::from_uri(URI).is_ok());
588        assert!(TrackId::from_uri(URI_SLASHES).is_ok());
589        assert_eq!(TrackId::from_uri(ID), Err(IdError::InvalidPrefix));
590        assert_eq!(TrackId::from_uri(URI_SHORT), Err(IdError::InvalidPrefix));
591        assert_eq!(TrackId::from_uri(URI_EMPTY), Err(IdError::InvalidType));
592        assert_eq!(TrackId::from_uri(URI_WRONGTYPE1), Err(IdError::InvalidType));
593        assert_eq!(TrackId::from_uri(URI_MIXED1), Err(IdError::InvalidFormat));
594        assert_eq!(TrackId::from_uri(URI_MIXED2), Err(IdError::InvalidFormat));
595    }
596
597    /// Deserialization should accept both IDs and URIs as well.
598    #[test]
599    fn test_id_or_uri_and_deserialize() {
600        fn test_any<F, E>(check: F)
601        where
602            F: Fn(&str) -> Result<TrackId<'_>, E>,
603            E: Error,
604        {
605            // In this case we also check that the contents are the ID and not
606            // the URI.
607            assert!(check(ID).is_ok());
608            assert_eq!(check(ID).unwrap().id(), ID);
609            assert!(check(URI).is_ok());
610            assert_eq!(check(URI).unwrap().id(), ID);
611            assert!(check(URI_SLASHES).is_ok());
612            assert_eq!(check(URI_SLASHES).unwrap().id(), ID);
613
614            // These should not work in any case
615            assert!(check(URI_SHORT).is_err());
616            assert!(check(URI_EMPTY).is_err());
617            assert!(check(URI_WRONGTYPE1).is_err());
618            assert!(check(URI_MIXED1).is_err());
619            assert!(check(URI_MIXED2).is_err());
620        }
621
622        // Easily testing both ways to obtain an ID
623        test_any(|s| TrackId::from_id_or_uri(s));
624        test_any(|s| {
625            let json = format!("\"{s}\"");
626            serde_json::from_str::<'_, TrackId>(&json)
627        });
628    }
629
630    /// Serializing should return the Id within it, not the URI.
631    #[test]
632    fn test_serialize() {
633        let json_expected = format!("\"{ID}\"");
634        let track = TrackId::from_uri(URI).unwrap();
635        let json = serde_json::to_string(&track).unwrap();
636        assert_eq!(json, json_expected);
637    }
638
639    #[test]
640    fn test_multiple_types() {
641        fn endpoint<'a>(_ids: impl IntoIterator<Item = PlayableId<'a>>) {}
642
643        let tracks: Vec<PlayableId> = vec![
644            PlayableId::Track(TrackId::from_id(ID).unwrap()),
645            PlayableId::Track(TrackId::from_id(ID).unwrap()),
646            PlayableId::Episode(EpisodeId::from_id(ID).unwrap()),
647            PlayableId::Episode(EpisodeId::from_id(ID).unwrap()),
648        ];
649        endpoint(tracks);
650    }
651
652    #[test]
653    fn test_unknown_at_compile_time() {
654        fn endpoint1(input: &str, is_episode: bool) -> PlayableId<'_> {
655            if is_episode {
656                PlayableId::Episode(EpisodeId::from_id(input).unwrap())
657            } else {
658                PlayableId::Track(TrackId::from_id(input).unwrap())
659            }
660        }
661        fn endpoint2(_id: &[PlayableId]) {}
662
663        let id = endpoint1(ID, false);
664        endpoint2(&[id]);
665    }
666
667    #[test]
668    fn test_constructor() {
669        // With `&str`
670        let _ = EpisodeId::from_id(ID).unwrap();
671        // With `String`
672        let _ = EpisodeId::from_id(ID.to_string()).unwrap();
673        // With borrowed `Cow<str>`
674        let _ = EpisodeId::from_id(Cow::Borrowed(ID)).unwrap();
675        // With owned `Cow<str>`
676        let _ = EpisodeId::from_id(Cow::Owned(ID.to_string())).unwrap();
677    }
678
679    #[test]
680    fn test_owned() {
681        // We check it twice to make sure cloning statically also works.
682        fn check_static(_: EpisodeId<'static>) {}
683
684        // With lifetime smaller than static because it's a locally owned
685        // variable.
686        let local_id = String::from(ID);
687
688        // With `&str`: should be converted
689        let id: EpisodeId<'_> = EpisodeId::from_id(local_id.as_str()).unwrap();
690        check_static(id.clone_static());
691        check_static(id.into_static());
692
693        // With `String`: already static
694        let id = EpisodeId::from_id(local_id.clone()).unwrap();
695        check_static(id.clone());
696        check_static(id);
697    }
698}