Decide carefully where external::musicbrainz belongs #196

Merged
wojtek merged 11 commits from 193---decide-carefully-where-external--musicbrainz-belongs into main 2024-08-28 18:21:13 +02:00
7 changed files with 304 additions and 215 deletions
Showing only changes of commit 97e8941e90 - Show all commits

View File

@ -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::<Uuid>::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::<Uuid>::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());

View File

@ -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::<Mbid>::try_into("i-am-not-a-uuid").unwrap_err();

View File

@ -54,7 +54,7 @@ impl<'a> LookupArtistRequest<'a> {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub struct LookupArtistResponse {
pub release_groups: Vec<LookupArtistResponseReleaseGroup>,
}
@ -73,7 +73,7 @@ impl From<DeserializeLookupArtistResponse> 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<AlbumSecondaryType>,
}
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeLookupArtistResponseReleaseGroup {
id: SerdeMbid,
@ -103,3 +103,61 @@ impl From<DeserializeLookupArtistResponseReleaseGroup> 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);
}
}

View File

@ -68,6 +68,7 @@ impl<Http> MusicBrainzClient<Http> {
}
}
#[derive(Clone, Debug)]
pub struct SerdeMbid(Mbid);
impl From<SerdeMbid> for Mbid {
@ -105,6 +106,7 @@ impl<'de> Deserialize<'de> for SerdeMbid {
}
}
#[derive(Debug, Clone)]
pub struct SerdeAlbumDate(AlbumDate);
impl From<SerdeAlbumDate> 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<SerdeAlbumPrimaryType> for AlbumPrimaryType {
@ -179,12 +181,6 @@ impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
}
}
impl From<AlbumPrimaryType> 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<SerdeAlbumSecondaryType> for AlbumSecondaryType {
@ -215,8 +211,99 @@ impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
}
}
impl From<AlbumSecondaryType> 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::<Null>::format_album_date(&AlbumDate::new(None, None, None)),
None
);
assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&(1986).into()),
Some(String::from("1986"))
);
assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&(1986, 4).into()),
Some(String::from("1986-04"))
);
assert_eq!(
MusicBrainzClient::<Null>::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<SerdeMbid, _> = 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<SerdeAlbumDate, _> = 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);
}
}

View File

@ -26,7 +26,7 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
let mut query: Vec<String> = 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<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
}
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<SearchReleaseGroupResponseReleaseGroup>,
}
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchReleaseGroupResponse {
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
@ -105,7 +105,7 @@ impl From<DeserializeSearchReleaseGroupResponse> 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<Vec<AlbumSecondaryType>>,
}
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchReleaseGroupResponseReleaseGroup {
score: u8,
@ -142,3 +142,131 @@ impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
}
}
}
#[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();
}
}

View File

@ -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::<ResponseLookupArtist>()
// .times(1)
// .return_once(|_| Err(ClientError::Client(String::from("get rekt scrub"))));
// client
// .expect_get::<ResponseLookupArtist>()
// .times(1)
// .return_once(|_| Err(ClientError::Status(503)));
// client
// .expect_get::<ResponseLookupArtist>()
// .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);
// }
// }

View File

@ -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::{