lastfm/
recent_tracks_page.rs

1//! # Recent tracks page
2//!
3//! Defines the [`RecentTracksPage`] struct and its methods.
4use crate::{error_response::ErrorResponse, track::Track};
5use serde::{de::Error, Deserialize, Deserializer, Serialize};
6use serde_json::Value;
7
8/// A Last.fm recent tracks response. Can either be an error or an actual page of recent tracks.
9#[derive(Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum RecentTracksResponse {
12    Error(ErrorResponse),
13    RecentTracksPage(RecentTracksPage),
14}
15
16/// A Last.fm recent tracks page.
17#[derive(Serialize, Debug, Clone)]
18pub struct RecentTracksPage {
19    pub total_tracks: u64,
20    pub tracks: Vec<Track>,
21}
22
23impl<'de> Deserialize<'de> for RecentTracksPage {
24    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
25    where
26        D: Deserializer<'de>,
27    {
28        let raw_data: Value = Deserialize::deserialize(deserializer)?;
29
30        // deserialize recenttracks,
31        let raw_recent_tracks = raw_data
32            .get("recenttracks")
33            .ok_or_else(|| D::Error::missing_field("recenttracks"))?
34            .as_object()
35            .ok_or_else(|| D::Error::custom("Field recenttracks is not an object"))?;
36
37        // deserialize total_tracks,
38        let total_tracks = raw_recent_tracks
39            .get("@attr")
40            .ok_or_else(|| D::Error::missing_field("@attr"))?
41            .as_object()
42            .ok_or_else(|| D::Error::custom("Field @attr is not an object"))?
43            .get("total")
44            .ok_or_else(|| D::Error::missing_field("total"))?
45            .as_str()
46            .ok_or_else(|| D::Error::custom("Field total is not a string"))?
47            .parse::<u64>()
48            .map_err(|e| D::Error::custom(format!("Failed to parse total: {e}")))?;
49
50        // deserialize tracks,
51        let tracks = raw_recent_tracks
52            .get("track")
53            .ok_or_else(|| D::Error::missing_field("track"))?
54            .as_array()
55            .ok_or_else(|| D::Error::custom("Field track is not an array"))?
56            .iter()
57            .map(|t| {
58                serde_json::from_value::<Track>(t.clone())
59                    .map_err(|e| D::Error::custom(format!("Cannot deserialize track: {e}")))
60            })
61            .collect::<Result<Vec<Track>, D::Error>>()?;
62
63        Ok(RecentTracksPage {
64            total_tracks,
65            tracks,
66        })
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn it_deserializes_a_recent_tracks_page() {
76        let json_data = include_str!("fixtures/recent_tracks_page.json");
77
78        let recent_tracks_page: RecentTracksPage = serde_json::from_str(json_data).unwrap();
79        insta::assert_debug_snapshot!(recent_tracks_page);
80    }
81}