diff --git a/src/core/collection/musicbrainz.rs b/src/core/collection/musicbrainz.rs index ff2f78f..ae73f74 100644 --- a/src/core/collection/musicbrainz.rs +++ b/src/core/collection/musicbrainz.rs @@ -133,6 +133,11 @@ mod tests { assert_eq!(url_str, mb.url().as_ref()); assert_eq!(uuid, mb.mbid().uuid().to_string()); + let mbid: Mbid = TryInto::::try_into(uuid).unwrap().into(); + let mb: MbArtistRef = mbid.into(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); + let url: Url = url_str.as_str().try_into().unwrap(); let mb: MbArtistRef = url.try_into().unwrap(); assert_eq!(url_str, mb.url().as_ref()); @@ -152,6 +157,11 @@ mod tests { assert_eq!(url_str, mb.url().as_ref()); assert_eq!(uuid, mb.mbid().uuid().to_string()); + let mbid: Mbid = TryInto::::try_into(uuid).unwrap().into(); + let mb: MbAlbumRef = mbid.into(); + assert_eq!(url_str, mb.url().as_ref()); + assert_eq!(uuid, mb.mbid().uuid().to_string()); + let url: Url = url_str.as_str().try_into().unwrap(); let mb: MbAlbumRef = url.try_into().unwrap(); assert_eq!(url_str, mb.url().as_ref()); diff --git a/src/core/interface/musicbrainz/mod.rs b/src/core/interface/musicbrainz/mod.rs index d586e23..d8b4f7a 100644 --- a/src/core/interface/musicbrainz/mod.rs +++ b/src/core/interface/musicbrainz/mod.rs @@ -48,7 +48,6 @@ try_from_impl_for_mbid!(&str); try_from_impl_for_mbid!(&String); try_from_impl_for_mbid!(String); - #[test] fn errors() { let mbid_err: MbidError = TryInto::::try_into("i-am-not-a-uuid").unwrap_err(); diff --git a/src/external/musicbrainz/api/lookup.rs b/src/external/musicbrainz/api/lookup.rs index f5e7ec0..70968e8 100644 --- a/src/external/musicbrainz/api/lookup.rs +++ b/src/external/musicbrainz/api/lookup.rs @@ -54,7 +54,7 @@ impl<'a> LookupArtistRequest<'a> { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct LookupArtistResponse { pub release_groups: Vec, } @@ -73,7 +73,7 @@ impl From for LookupArtistResponse { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct LookupArtistResponseReleaseGroup { pub id: Mbid, pub title: String, @@ -82,7 +82,7 @@ pub struct LookupArtistResponseReleaseGroup { pub secondary_types: Vec, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeLookupArtistResponseReleaseGroup { id: SerdeMbid, @@ -103,3 +103,61 @@ impl From for LookupArtistResponseR } } } + +#[cfg(test)] +mod tests { + use mockall::predicate; + + use crate::external::musicbrainz::MockIMusicBrainzHttp; + + use super::*; + + #[test] + fn lookup_artist() { + let mut http = MockIMusicBrainzHttp::new(); + let url = format!( + "https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups", + mbid = "00000000-0000-0000-0000-000000000000", + ); + + let de_release_group = DeserializeLookupArtistResponseReleaseGroup { + id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), + title: String::from("an album"), + first_release_date: SerdeAlbumDate((1986, 4).into()), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)], + }; + let de_response = DeserializeLookupArtistResponse { + release_groups: vec![de_release_group.clone()], + }; + + let release_group = LookupArtistResponseReleaseGroup { + id: de_release_group.id.0, + title: de_release_group.title, + first_release_date: de_release_group.first_release_date.0, + primary_type: de_release_group.primary_type.0, + secondary_types: de_release_group + .secondary_types + .into_iter() + .map(|st| st.0) + .collect(), + }; + let response = LookupArtistResponse { + release_groups: vec![release_group], + }; + + http.expect_get() + .times(1) + .with(predicate::eq(url)) + .return_once(|_| Ok(de_response)); + + let mut client = MusicBrainzClient::new(http); + + let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let mut request = LookupArtistRequest::new(&mbid); + request.include_release_groups(); + let result = client.lookup_artist(request).unwrap(); + + assert_eq!(result, response); + } +} diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs index 0c1cc43..9291561 100644 --- a/src/external/musicbrainz/api/mod.rs +++ b/src/external/musicbrainz/api/mod.rs @@ -68,6 +68,7 @@ impl MusicBrainzClient { } } +#[derive(Clone, Debug)] pub struct SerdeMbid(Mbid); impl From for Mbid { @@ -105,6 +106,7 @@ impl<'de> Deserialize<'de> for SerdeMbid { } } +#[derive(Debug, Clone)] pub struct SerdeAlbumDate(AlbumDate); impl From for AlbumDate { @@ -170,7 +172,7 @@ pub enum AlbumPrimaryTypeDef { Other, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType); impl From for AlbumPrimaryType { @@ -179,12 +181,6 @@ impl From for AlbumPrimaryType { } } -impl From for SerdeAlbumPrimaryType { - fn from(value: AlbumPrimaryType) -> Self { - SerdeAlbumPrimaryType(value) - } -} - #[derive(Debug, Deserialize)] #[serde(remote = "AlbumSecondaryType")] pub enum AlbumSecondaryTypeDef { @@ -206,7 +202,7 @@ pub enum AlbumSecondaryTypeDef { FieldRecording, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType); impl From for AlbumSecondaryType { @@ -215,8 +211,99 @@ impl From for AlbumSecondaryType { } } -impl From for SerdeAlbumSecondaryType { - fn from(value: AlbumSecondaryType) -> Self { - SerdeAlbumSecondaryType(value) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn errors() { + let http_err = HttpError::Client(String::from("a http error")); + let http_err: Error = http_err.into(); + assert!(matches!(http_err, Error::Http(_))); + assert!(!http_err.to_string().is_empty()); + assert!(!format!("{http_err:?}").is_empty()); + + let rate_err = HttpError::Status(MB_RATE_LIMIT_CODE); + let rate_err: Error = rate_err.into(); + assert!(matches!(rate_err, Error::RateLimit)); + assert!(!rate_err.to_string().is_empty()); + assert!(!format!("{rate_err:?}").is_empty()); + + let unk_err = HttpError::Status(404); + let unk_err: Error = unk_err.into(); + assert!(matches!(unk_err, Error::Unknown(_))); + assert!(!unk_err.to_string().is_empty()); + assert!(!format!("{unk_err:?}").is_empty()); + } + + #[test] + fn format_album_date() { + struct Null; + assert_eq!( + MusicBrainzClient::::format_album_date(&AlbumDate::new(None, None, None)), + None + ); + assert_eq!( + MusicBrainzClient::::format_album_date(&(1986).into()), + Some(String::from("1986")) + ); + assert_eq!( + MusicBrainzClient::::format_album_date(&(1986, 4).into()), + Some(String::from("1986-04")) + ); + assert_eq!( + MusicBrainzClient::::format_album_date(&(1986, 4, 21).into()), + Some(String::from("1986-04-21")) + ); + } + + #[test] + fn serde() { + let mbid = "\"d368baa8-21ca-4759-9731-0b2753071ad8\""; + let mbid: SerdeMbid = serde_json::from_str(mbid).unwrap(); + let mbid: Mbid = mbid.into(); + assert_eq!( + mbid, + "d368baa8-21ca-4759-9731-0b2753071ad8".try_into().unwrap() + ); + + let mbid = "0"; + let result: Result = serde_json::from_str(mbid); + assert!(result + .unwrap_err() + .to_string() + .contains("a valid MusicBrainz identifier")); + + let album_date = "\"1986-04-21\""; + let album_date: SerdeAlbumDate = serde_json::from_str(album_date).unwrap(); + let album_date: AlbumDate = album_date.into(); + assert_eq!(album_date, AlbumDate::new(Some(1986), Some(4), Some(21))); + + let album_date = "\"1986-04\""; + let album_date: SerdeAlbumDate = serde_json::from_str(album_date).unwrap(); + let album_date: AlbumDate = album_date.into(); + assert_eq!(album_date, AlbumDate::new(Some(1986), Some(4), None)); + + let album_date = "\"1986\""; + let album_date: SerdeAlbumDate = serde_json::from_str(album_date).unwrap(); + let album_date: AlbumDate = album_date.into(); + assert_eq!(album_date, AlbumDate::new(Some(1986), None, None)); + + let album_date = "0"; + let result: Result = serde_json::from_str(album_date); + assert!(result + .unwrap_err() + .to_string() + .contains("a valid YYYY(-MM-(-DD)) date")); + + let primary_type = "\"EP\""; + let primary_type: SerdeAlbumPrimaryType = serde_json::from_str(primary_type).unwrap(); + let primary_type: AlbumPrimaryType = primary_type.into(); + assert_eq!(primary_type, AlbumPrimaryType::Ep); + + let secondary_type = "\"Field recording\""; + let secondary_type: SerdeAlbumSecondaryType = serde_json::from_str(secondary_type).unwrap(); + let secondary_type: AlbumSecondaryType = secondary_type.into(); + assert_eq!(secondary_type, AlbumSecondaryType::FieldRecording); } } diff --git a/src/external/musicbrainz/api/search.rs b/src/external/musicbrainz/api/search.rs index 2121a0d..5378300 100644 --- a/src/external/musicbrainz/api/search.rs +++ b/src/external/musicbrainz/api/search.rs @@ -26,7 +26,7 @@ impl MusicBrainzClient { let mut query: Vec = vec![]; if let Some(arid) = request.arid { - query.push(format!("arid:{}", arid.uuid().as_hyphenated().to_string())); + query.push(format!("arid:{}", arid.uuid().as_hyphenated())); } if let Some(date) = request.first_release_date { @@ -40,7 +40,7 @@ impl MusicBrainzClient { } if let Some(rgid) = request.rgid { - query.push(format!("rgid:{}", rgid.uuid().as_hyphenated().to_string())); + query.push(format!("rgid:{}", rgid.uuid().as_hyphenated())); } let query: String = @@ -86,12 +86,12 @@ impl<'a> SearchReleaseGroupRequest<'a> { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct SearchReleaseGroupResponse { pub release_groups: Vec, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeSearchReleaseGroupResponse { release_groups: Vec, @@ -105,7 +105,7 @@ impl From for SearchReleaseGroupResponse } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchReleaseGroupResponseReleaseGroup { pub score: u8, pub id: Mbid, @@ -115,7 +115,7 @@ pub struct SearchReleaseGroupResponseReleaseGroup { pub secondary_types: Option>, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] struct DeserializeSearchReleaseGroupResponseReleaseGroup { score: u8, @@ -142,3 +142,131 @@ impl From } } } + +#[cfg(test)] +mod tests { + use mockall::{predicate, Sequence}; + + use crate::{collection::album::AlbumId, external::musicbrainz::MockIMusicBrainzHttp}; + + use super::*; + + #[test] + fn search_release_group() { + let mut http = MockIMusicBrainzHttp::new(); + let url_title = format!( + "https://musicbrainz.org/ws/2\ + /release-group\ + ?query=arid%3A{arid}+AND+firstreleasedate%3A{date}+AND+releasegroup%3A%22{title}%22", + arid = "00000000-0000-0000-0000-000000000000", + date = "1986-04", + title = "an+album", + ); + let url_rgid = format!( + "https://musicbrainz.org/ws/2\ + /release-group\ + ?query=rgid%3A{rgid}", + rgid = "11111111-1111-1111-1111-111111111111", + ); + + let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup { + score: 67, + id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), + title: String::from("an album"), + first_release_date: SerdeAlbumDate((1986, 4).into()), + primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]), + }; + let de_response = DeserializeSearchReleaseGroupResponse { + release_groups: vec![de_release_group.clone()], + }; + + let release_group = SearchReleaseGroupResponseReleaseGroup { + score: 67, + id: de_release_group.id.0, + title: de_release_group.title, + first_release_date: de_release_group.first_release_date.0, + primary_type: de_release_group.primary_type.0, + secondary_types: de_release_group + .secondary_types + .map(|v| v.into_iter().map(|st| st.0).collect()), + }; + let response = SearchReleaseGroupResponse { + release_groups: vec![release_group.clone()], + }; + + let mut seq = Sequence::new(); + + let title_response = de_response.clone(); + http.expect_get() + .times(1) + .with(predicate::eq(url_title)) + .return_once(|_| Ok(title_response)) + .in_sequence(&mut seq); + + let rgid_response = de_response; + http.expect_get() + .times(1) + .with(predicate::eq(url_rgid)) + .return_once(|_| Ok(rgid_response)) + .in_sequence(&mut seq); + + let mut client = MusicBrainzClient::new(http); + + let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let title: AlbumId = AlbumId::new("an album"); + let date = (1986, 4).into(); + + let mut request = SearchReleaseGroupRequest::new(); + request + .arid(&arid) + .release_group(&title.title) + .first_release_date(&date); + + let matches = client.search_release_group(request).unwrap(); + assert_eq!(matches, response); + + let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); + + let mut request = SearchReleaseGroupRequest::new(); + request.rgid(&rgid); + + let matches = client.search_release_group(request).unwrap(); + assert_eq!(matches, response); + } + + #[test] + fn search_release_group_empty_date() { + let mut http = MockIMusicBrainzHttp::new(); + let url = format!( + "https://musicbrainz.org/ws/2\ + /release-group\ + ?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22", + arid = "00000000-0000-0000-0000-000000000000", + title = "an+album", + ); + + let de_response = DeserializeSearchReleaseGroupResponse { + release_groups: vec![], + }; + + http.expect_get() + .times(1) + .with(predicate::eq(url)) + .return_once(|_| Ok(de_response)); + + let mut client = MusicBrainzClient::new(http); + + let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + let title: AlbumId = AlbumId::new("an album"); + let date = AlbumDate::default(); + + let mut request = SearchReleaseGroupRequest::new(); + request + .arid(&arid) + .release_group(&title.title) + .first_release_date(&date); + + let _ = client.search_release_group(request).unwrap(); + } +} diff --git a/src/external/musicbrainz/mod.rs b/src/external/musicbrainz/mod.rs index 5ef9502..96fe786 100644 --- a/src/external/musicbrainz/mod.rs +++ b/src/external/musicbrainz/mod.rs @@ -17,196 +17,3 @@ pub enum HttpError { Client(String), Status(u16), } - -// #[cfg(test)] -// mod tests { -// use mockall::{predicate, Sequence}; - -// use crate::collection::album::AlbumId; - -// use super::*; - -// #[test] -// fn errors() { -// let client_err: Error = Error::Client(String::from("a client error")); -// assert!(!client_err.to_string().is_empty()); -// assert!(!format!("{client_err:?}").is_empty()); - -// let rate_err: Error = Error::RateLimit; -// assert!(!rate_err.to_string().is_empty()); -// assert!(!format!("{rate_err:?}").is_empty()); - -// let unk_err: Error = Error::Unknown(404); -// assert!(!unk_err.to_string().is_empty()); -// assert!(!format!("{unk_err:?}").is_empty()); -// } - -// #[test] -// fn lookup_artist_release_group() { -// let mut client = MockIMusicBrainzClient::new(); -// let url = format!( -// "https://musicbrainz.org/ws/2/artist/{mbid}?inc=release-groups", -// mbid = "00000000-0000-0000-0000-000000000000", -// ); - -// let release_group = LookupReleaseGroup { -// id: String::from("11111111-1111-1111-1111-111111111111"), -// title: String::from("an album"), -// first_release_date: String::from("1986-04"), -// primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), -// secondary_types: vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Compilation)], -// }; -// let response = ResponseLookupArtist { -// release_groups: vec![release_group], -// }; - -// client -// .expect_get() -// .times(1) -// .with(predicate::eq(url)) -// .return_once(|_| Ok(response)); - -// let mut api = MusicBrainz::new(client); - -// let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); -// let results = api.lookup_artist_release_groups(&mbid).unwrap(); - -// let mut album = Album::new( -// AlbumId::new("an album"), -// (1986, 4), -// Some(AlbumPrimaryType::Album), -// vec![AlbumSecondaryType::Compilation], -// ); -// album.set_musicbrainz_ref( -// MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(), -// ); -// let expected = vec![album]; - -// assert_eq!(results, expected); -// } - -// #[test] -// fn search_release_group() { -// let mut client = MockIMusicBrainzClient::new(); -// let url_title = format!( -// "https://musicbrainz.org/ws/2\ -// /release-group\ -// ?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22+AND+firstreleasedate%3A{year}", -// arid = "00000000-0000-0000-0000-000000000000", -// title = "an+album", -// year = "1986" -// ); -// let url_rgid = format!( -// "https://musicbrainz.org/ws/2\ -// /release-group\ -// ?query=arid%3A{arid}+AND+rgid%3A{rgid}", -// arid = "00000000-0000-0000-0000-000000000000", -// rgid = "11111111-1111-1111-1111-111111111111", -// ); - -// let release_group = SearchReleaseGroup { -// score: 67, -// id: String::from("11111111-1111-1111-1111-111111111111"), -// title: String::from("an album"), -// first_release_date: String::from("1986-04"), -// primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), -// secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]), -// }; -// let response = ResponseSearchReleaseGroup { -// release_groups: vec![release_group], -// }; - -// // For code coverage of derive(Debug). -// assert!(!format!("{response:?}").is_empty()); - -// let mut seq = Sequence::new(); - -// let title_response = response.clone(); -// client -// .expect_get() -// .times(1) -// .with(predicate::eq(url_title)) -// .return_once(|_| Ok(title_response)) -// .in_sequence(&mut seq); - -// let rgid_response = response; -// client -// .expect_get() -// .times(1) -// .with(predicate::eq(url_rgid)) -// .return_once(|_| Ok(rgid_response)) -// .in_sequence(&mut seq); - -// let mut album = Album::new( -// AlbumId::new("an album"), -// (1986, 4), -// Some(AlbumPrimaryType::Album), -// vec![AlbumSecondaryType::Live], -// ); -// album.set_musicbrainz_ref( -// MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(), -// ); -// let expected = vec![Match::new(67, album)]; - -// let mut api = MusicBrainz::new(client); - -// let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); -// let mut album = Album::new(AlbumId::new("an album"), (1986, 4), None, vec![]); -// let matches = api.search_release_group(&arid, &album).unwrap(); -// assert_eq!(matches, expected); - -// let rgid = MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(); -// album.set_musicbrainz_ref(rgid); -// let matches = api.search_release_group(&arid, &album).unwrap(); -// assert_eq!(matches, expected); -// } - -// #[test] -// fn client_errors() { -// let mut client = MockIMusicBrainzClient::new(); - -// let error = ClientError::Client(String::from("get rekt")); -// assert!(!format!("{error:?}").is_empty()); - -// client -// .expect_get::() -// .times(1) -// .return_once(|_| Err(ClientError::Client(String::from("get rekt scrub")))); - -// client -// .expect_get::() -// .times(1) -// .return_once(|_| Err(ClientError::Status(503))); - -// client -// .expect_get::() -// .times(1) -// .return_once(|_| Err(ClientError::Status(504))); - -// let mut api = MusicBrainz::new(client); - -// let mbid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap(); - -// let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); -// assert_eq!(error, Error::Client(String::from("get rekt scrub"))); - -// let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); -// assert_eq!(error, Error::RateLimit); - -// let error = api.lookup_artist_release_groups(&mbid).unwrap_err(); -// assert_eq!(error, Error::Unknown(504)); -// } - -// #[test] -// fn serde() { -// let primary_type = "\"EP\""; -// let primary_type: SerdeAlbumPrimaryType = serde_json::from_str(primary_type).unwrap(); -// let primary_type: AlbumPrimaryType = primary_type.into(); -// assert_eq!(primary_type, AlbumPrimaryType::Ep); - -// let secondary_type = "\"Field recording\""; -// let secondary_type: SerdeAlbumSecondaryType = serde_json::from_str(secondary_type).unwrap(); -// let secondary_type: AlbumSecondaryType = secondary_type.into(); -// assert_eq!(secondary_type, AlbumSecondaryType::FieldRecording); -// } -// } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 69b2ad3..f25baa2 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -8,8 +8,8 @@ mod ui; pub use app::App; pub use event::EventChannel; pub use handler::EventHandler; -pub use listener::EventListener; pub use lib::external::musicbrainz::MusicBrainz; +pub use listener::EventListener; pub use ui::Ui; use crossterm::{