Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Struct refactor #10

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
reqwest = { version = "0.11.2", features = ["json"] }
url = "2.2.1"
void = "1.0.2"
85 changes: 85 additions & 0 deletions src/model/custom_deserialization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// The following code is adapted from https://serde.rs/string-or-struct.html
// as it appears on 2021-05-29

// the `string_or_struct` function uses these impl to instantiate a Type
// if the input file contains a string and not a struct.

use serde::{Deserialize, Deserializer};
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use void::Void;

pub fn option_string_or_struct<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: Deserialize<'de> + FromStr<Err = Void>,
D: Deserializer<'de>,
{
struct OptionStringOrStruct<T>(PhantomData<fn() -> T>);

impl<'de, T> Visitor<'de> for OptionStringOrStruct<Option<T>>
where
T: Deserialize<'de> + FromStr<Err = Void>,
{
type Value = Option<T>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("null, string or map")
}

fn visit_str<E>(self, value: &str) -> Result<Option<T>, E>
where
E: de::Error,
{
Ok(Some(FromStr::from_str(value).unwrap()))
}

fn visit_map<M>(self, map: M) -> Result<Option<T>, M::Error>
where
M: MapAccess<'de>
{
match Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) {
Ok(res) => Ok(Some(res)),
Err(err) => Err(err),
}
}
}

deserializer.deserialize_any(OptionStringOrStruct(PhantomData))
}

pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + FromStr<Err = Void>,
D: Deserializer<'de>,
{
struct StringOrStruct<T>(PhantomData<fn() -> T>);

impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: Deserialize<'de> + FromStr<Err = Void>,
{
type Value = T;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}

fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}

fn visit_map<M>(self, map: M) -> Result<T, M::Error>
where
M: MapAccess<'de>
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
}
}

deserializer.deserialize_any(StringOrStruct(PhantomData))
}
114 changes: 114 additions & 0 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
//! These are the various models the crate uses throughout the library, centralized
//! in this file to ease development and remove code duplication.

use crate::model::custom_deserialization::{string_or_struct, option_string_or_struct};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::str::FromStr;
use void::Void;

use crate::utilities::deserialize_datetime_from_str;

pub mod custom_deserialization;

/// Various attributes transmitted by several API endpoints.
#[derive(Debug, Deserialize)]
pub struct Attributes {
Expand Down Expand Up @@ -55,3 +60,112 @@ pub struct Image {
#[serde(rename = "#text")]
pub image_url: String,
}

/// Contains information about the given track
#[derive(Debug, Deserialize)]
pub struct Track {
/// The artist who published the given track.
#[serde(deserialize_with = "string_or_struct")]
pub artist: Artist,
/// Various attributes associated with the track.
#[serde(rename = "@attr")]
pub attrs: Option<TrackAttributes>,
/// The MusicBrainz ID for the given track.
pub mbid: Option<String>,
/// The name of the track.
pub name: String,
/// The album the track is associated with.
pub album: Option<Album>,
/// The last.fm URL of the track.
pub url: String,
/// Images associated with the track.
#[serde(rename = "image")]
pub images: Vec<Image>,
/// The date of when the track was scrobbled.
/// Returned when output comes from some endpoints such as loved_tracks
pub date: Option<TrackDate>,
/// Whether or not the track is streamable
#[serde(deserialize_with = "option_string_or_struct")]
pub streamable: Option<Streamable>,
/// The match for the given track
/// Returned when output comes from some endpoints such as similar
pub r#match: Option<f32>,
}

#[derive(Debug, Deserialize)]
pub struct TrackAttributes {
/// Whether or not the user's first available track is the
/// one the user is currently playing.
#[serde(rename = "nowplaying")]
pub now_playing: Option<String>,
}

impl FromStr for Streamable {
type Err = Void;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Streamable {
text: s.to_string(),
fulltrack: None,
})
}
}

#[derive(Debug, Deserialize)]
pub struct Album {
/// The name of the album.
#[serde(rename = "#text")]
pub name: String,
}

#[derive(Debug, Deserialize)]
pub struct Artist {
/// The name of the artist.
#[serde(alias = "#text")]
pub name: String,
/// The last.fm URL for the given artist.
pub url: Option<String>,
/// The MusicBrainz ID of the given artist.
pub mbid: Option<String>,
/// Attributes associated with the artist.
#[serde(rename = "@attr")]
pub attrs: Option<ArtistAttributes>,
/// How many times the user has scrobbled the artist.
#[serde(rename = "playcount")]
pub scrobbles: Option<String>,
/// The main images linked to the artist.
#[serde(rename = "image")]
pub images: Option<Vec<Image>>,
}

impl FromStr for Artist {
type Err = Void;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Artist {
name: s.to_string(),
url: None,
mbid: None,
attrs: None,
scrobbles: None,
images: None,
})
}
}

#[derive(Debug, Deserialize)]
pub struct ArtistAttributes {
/// Where the artist is ranked in the user's profile.
pub rank: Option<String>,
}

/// The streamable struct.
///
/// Available if the given track is available for streaming.
#[derive(Debug, Deserialize)]
pub struct Streamable {
pub fulltrack: Option<String>,
#[serde(rename = "#text")]
pub text: String,
}

2 changes: 1 addition & 1 deletion src/track/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::Deserialize;
pub mod similar;

#[derive(Debug, Deserialize)]
pub struct Track {
pub struct Endpoints {
#[serde(rename = "similartracks")]
pub similar_tracks: Option<similar::Similar>,
}
46 changes: 3 additions & 43 deletions src/track/similar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::marker::PhantomData;

use crate::{
error::{Error, LastFMError},
model::{Attributes, Image},
track::{Track as OriginalTrack},
model::Track,
track::Endpoints,
Client, RequestBuilder,
};

Expand All @@ -23,46 +23,6 @@ pub struct Similar {
pub tracks: Vec<Track>,
}

#[derive(Debug, Deserialize)]
pub struct Track {
/// The name of the given track
pub name: String,
/// The MusicBrainz ID for the given track.
pub mbid: Option<String>,
/// The match for the given track
pub r#match: f32,
/// The Last.fm URL of the track
pub url: String,
/// Whether or not the track is streamable
pub streamable: Streamable,
/// the artist who published the given track
pub artist: Artist,
/// The cover art for the given track. Available in small, medium,
/// and large sizes.
#[serde(rename="image")]
pub images: Vec<Image>,
}

#[derive(Debug, Deserialize)]
pub struct Artist {
/// The name of the given artist
pub name: String,
/// The MusicBrainz ID of the given artist
pub mbid: Option<String>,
/// The Last.fm URL for the given artist
pub url: String,
}

/// The streamable struct.
///
/// Available if the given track is available for streaming.
#[derive(Debug, Deserialize)]
pub struct Streamable {
pub fulltrack: String,
#[serde(rename = "#text")]
pub text: String,
}

impl Similar {
pub async fn build_by_mbid<'a>(client: &'a mut Client, mbid: &str) -> RequestBuilder<'a, Similar> {
let url = client.build_url(vec![("method", "track.getSimilar"), ("mbid", mbid)]).await;
Expand All @@ -84,7 +44,7 @@ impl<'a> RequestBuilder<'a, Similar> {
let body = response.text().await.unwrap();
match serde_json::from_str::<LastFMError>(&body) {
Ok(lastfm_error) => Err(Error::LastFMError(lastfm_error.into())),
Err(_) => match serde_json::from_str::<OriginalTrack>(&body) {
Err(_) => match serde_json::from_str::<Endpoints>(&body) {
Ok(tracks) => Ok(tracks.similar_tracks.unwrap()),
Err(e) => Err(Error::ParsingError(e)),
},
Expand Down
46 changes: 2 additions & 44 deletions src/user/loved_tracks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::marker::PhantomData;

use crate::{
error::{Error, LastFMError},
model::{Attributes, Image, TrackDate},
user::User,
model::Attributes,
user::{User, Track},
Client, RequestBuilder,
};

Expand All @@ -28,48 +28,6 @@ pub struct LovedTracks {
pub tracks: Vec<Track>,
}

/// Contains information about the given track that the
/// user Loved.
#[derive(Debug, Deserialize)]
pub struct Track {
/// The artist who published the given track.
pub artist: Artist,
/// The MusicBrainz ID for the given track.
pub mbid: String,
/// The date of when the user loved the track.
pub date: Option<TrackDate>,
/// The name of the track.
pub name: String,
/// The Last.fm URL of the track.
pub url: String,
/// The cover art for the given track. Available in small, medium,
/// and large sizes.
#[serde(rename = "image")]
pub images: Vec<Image>,
/// Whether or not the track is streamable.
pub streamable: Streamable,
}

#[derive(Debug, Deserialize)]
pub struct Artist {
/// The Last.fm URL for the given artist.
pub url: String,
/// The name of the given artist.
pub name: String,
/// The MusicBrainz ID of the given artist.
pub mbid: String,
}

/// The streamable struct.
///
/// Available if the given track is available for streaming.
#[derive(Debug, Deserialize)]
pub struct Streamable {
pub fulltrack: String,
#[serde(rename = "#text")]
pub text: String,
}

impl LovedTracks {
pub async fn build<'a>(client: &'a mut Client, user: &str) -> RequestBuilder<'a, LovedTracks> {
let url = client.build_url(vec![("method", "user.getLovedTracks"), ("user", user)]).await;
Expand Down
1 change: 1 addition & 0 deletions src/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Contains structs and methods related to working with the user-related endpoints
//! available through the Last.fm API.

use crate::model::{Track, Artist};
use serde::Deserialize;

pub mod loved_tracks;
Expand Down
Loading