Enable changs via command

This commit is contained in:
Wojciech Kozlowski 2024-03-03 20:36:13 +01:00
parent 0fee810040
commit adddc5ba2f
5 changed files with 314 additions and 116 deletions

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use structopt::{clap::AppSettings, StructOpt}; use structopt::{clap::AppSettings, StructOpt};
use musichoard::{ use musichoard::{
collection::artist::ArtistId, collection::{album::AlbumId, artist::ArtistId},
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
MusicHoard, MusicHoardBuilder, NoLibrary, MusicHoard, MusicHoardBuilder, NoLibrary,
}; };
@ -22,56 +22,54 @@ struct Opt {
database_file_path: PathBuf, database_file_path: PathBuf,
#[structopt(subcommand)] #[structopt(subcommand)]
category: Category, command: Command,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum Category { enum Command {
#[structopt(about = "Edit artist information")] #[structopt(about = "Modify artist information")]
Artist(ArtistCommand), Artist(ArtistOpt),
} }
impl Category { #[derive(StructOpt, Debug)]
fn handle(self, music_hoard: &mut MH) { struct ArtistOpt {
match self { // For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
Category::Artist(artist_command) => artist_command.handle(music_hoard), // for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
} // will insteady try to process it as a command.
} #[structopt(long, help = "The name of the artist")]
name: String,
#[structopt(subcommand)]
command: ArtistCommand,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum ArtistCommand { enum ArtistCommand {
#[structopt(about = "Add a new artist to the collection")] #[structopt(about = "Add a new artist to the collection")]
Add(ArtistValue), Add,
#[structopt(about = "Remove an artist from the collection")] #[structopt(about = "Remove an artist from the collection")]
Remove(ArtistValue), Remove,
#[structopt(about = "Edit the artist's sort name")] #[structopt(about = "Edit the artist's sort name")]
Sort(SortCommand), Sort(SortCommand),
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
MusicBrainz(MusicBrainzCommand), MusicBrainz(MusicBrainzCommand),
#[structopt(name = "property", about = "Edit a property of an artist")] #[structopt(about = "Edit a property of an artist")]
Property(PropertyCommand), Property(PropertyCommand),
#[structopt(about = "Modify the artist's album information")]
Album(AlbumOpt),
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum SortCommand { enum SortCommand {
#[structopt(about = "Set the provided name as the artist's sort name")] #[structopt(about = "Set the provided name as the artist's sort name")]
Set(ArtistSortValue), Set(SortValue),
#[structopt(about = "Clear the artist's sort name")] #[structopt(about = "Clear the artist's sort name")]
Clear(ArtistValue), Clear,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct ArtistValue { struct SortValue {
#[structopt(help = "The name of the artist")] #[structopt(help = "The sort name")]
artist: String,
}
#[derive(StructOpt, Debug)]
struct ArtistSortValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The sort name of the artist")]
sort: String, sort: String,
} }
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")] #[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
Set(MusicBrainzValue), Set(MusicBrainzValue),
#[structopt(about = "Clear the MusicBrainz URL)")] #[structopt(about = "Clear the MusicBrainz URL)")]
Clear(ArtistValue), Clear,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct MusicBrainzValue { struct MusicBrainzValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The MusicBrainz URL")] #[structopt(help = "The MusicBrainz URL")]
url: String, url: String,
} }
@ -105,8 +101,6 @@ enum PropertyCommand {
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct PropertyValue { struct PropertyValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")] #[structopt(help = "The name of the property")]
property: String, property: String,
#[structopt(help = "The list of values")] #[structopt(help = "The list of values")]
@ -115,101 +109,176 @@ struct PropertyValue {
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct PropertyName { struct PropertyName {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")] #[structopt(help = "The name of the property")]
property: String, property: String,
} }
impl ArtistCommand { #[derive(StructOpt, Debug)]
struct AlbumOpt {
// Using `long` for consistency with `ArtistOpt`.
#[structopt(long, help = "The title of the album")]
title: String,
#[structopt(subcommand)]
command: AlbumCommand,
}
#[derive(StructOpt, Debug)]
enum AlbumCommand {
#[structopt(about = "Edit the album's sequence value")]
Seq(AlbumSeqCommand),
}
#[derive(StructOpt, Debug)]
enum AlbumSeqCommand {
#[structopt(about = "Set the sequence value overwriting any existing value")]
Set(AlbumSeqValue),
#[structopt(about = "Clear the sequence value")]
Clear,
}
#[derive(StructOpt, Debug)]
struct AlbumSeqValue {
#[structopt(help = "The new sequence value")]
value: u8,
}
impl Command {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH) {
match self { match self {
ArtistCommand::Add(artist_value) => { Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
}
}
}
impl ArtistOpt {
fn handle(self, music_hoard: &mut MH) {
self.command.handle(music_hoard, &self.name)
}
}
impl ArtistCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
ArtistCommand::Add => {
music_hoard music_hoard
.add_artist(ArtistId::new(artist_value.artist)) .add_artist(ArtistId::new(artist_name))
.expect("failed to add artist"); .expect("failed to add artist");
} }
ArtistCommand::Remove(artist_value) => { ArtistCommand::Remove => {
music_hoard music_hoard
.remove_artist(ArtistId::new(artist_value.artist)) .remove_artist(ArtistId::new(artist_name))
.expect("failed to remove artist"); .expect("failed to remove artist");
} }
ArtistCommand::Sort(sort_command) => { ArtistCommand::Sort(sort_command) => {
sort_command.handle(music_hoard); sort_command.handle(music_hoard, artist_name);
} }
ArtistCommand::MusicBrainz(musicbrainz_command) => { ArtistCommand::MusicBrainz(musicbrainz_command) => {
musicbrainz_command.handle(music_hoard) musicbrainz_command.handle(music_hoard, artist_name)
} }
ArtistCommand::Property(property_command) => { ArtistCommand::Property(property_command) => {
property_command.handle(music_hoard); property_command.handle(music_hoard, artist_name);
}
ArtistCommand::Album(album_opt) => {
album_opt.handle(music_hoard, artist_name);
} }
} }
} }
} }
impl SortCommand { impl SortCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
SortCommand::Set(artist_sort_value) => music_hoard SortCommand::Set(artist_sort_value) => music_hoard
.set_artist_sort( .set_artist_sort(
ArtistId::new(artist_sort_value.artist), ArtistId::new(artist_name),
ArtistId::new(artist_sort_value.sort), ArtistId::new(artist_sort_value.sort),
) )
.expect("faild to set artist sort name"), .expect("faild to set artist sort name"),
SortCommand::Clear(artist_value) => music_hoard SortCommand::Clear => music_hoard
.clear_artist_sort(ArtistId::new(artist_value.artist)) .clear_artist_sort(ArtistId::new(artist_name))
.expect("failed to clear artist sort name"), .expect("failed to clear artist sort name"),
} }
} }
} }
impl MusicBrainzCommand { impl MusicBrainzCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
.set_musicbrainz_url( .set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
ArtistId::new(musicbrainz_value.artist),
musicbrainz_value.url,
)
.expect("failed to set MusicBrainz URL"), .expect("failed to set MusicBrainz URL"),
MusicBrainzCommand::Clear(artist_value) => music_hoard MusicBrainzCommand::Clear => music_hoard
.clear_musicbrainz_url(ArtistId::new(artist_value.artist)) .clear_artist_musicbrainz(ArtistId::new(artist_name))
.expect("failed to clear MusicBrainz URL"), .expect("failed to clear MusicBrainz URL"),
} }
} }
} }
impl PropertyCommand { impl PropertyCommand {
fn handle(self, music_hoard: &mut MH) { fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self { match self {
PropertyCommand::Add(property_value) => music_hoard PropertyCommand::Add(property_value) => music_hoard
.add_to_property( .add_to_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to add values to property"), .expect("failed to add values to property"),
PropertyCommand::Remove(property_value) => music_hoard PropertyCommand::Remove(property_value) => music_hoard
.remove_from_property( .remove_from_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to remove values from property"), .expect("failed to remove values from property"),
PropertyCommand::Set(property_value) => music_hoard PropertyCommand::Set(property_value) => music_hoard
.set_property( .set_artist_property(
ArtistId::new(property_value.artist), ArtistId::new(artist_name),
property_value.property, property_value.property,
property_value.values, property_value.values,
) )
.expect("failed to set property"), .expect("failed to set property"),
PropertyCommand::Clear(property_name) => music_hoard PropertyCommand::Clear(property_name) => music_hoard
.clear_property(ArtistId::new(property_name.artist), property_name.property) .clear_artist_property(ArtistId::new(artist_name), property_name.property)
.expect("failed to clear property"), .expect("failed to clear property"),
} }
} }
} }
impl AlbumOpt {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
self.command.handle(music_hoard, artist_name, &self.title)
}
}
impl AlbumCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumCommand::Seq(seq_command) => {
seq_command.handle(music_hoard, artist_name, album_name);
}
}
}
}
impl AlbumSeqCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumSeqCommand::Set(seq_value) => music_hoard
.set_album_seq(
ArtistId::new(artist_name),
AlbumId::new(album_name),
seq_value.value,
)
.expect("failed to set sequence value"),
AlbumSeqCommand::Clear => music_hoard
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
.expect("failed to clear sequence value"),
}
}
}
fn main() { fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
@ -219,5 +288,5 @@ fn main() {
.set_database(db) .set_database(db)
.build() .build()
.expect("failed to initialise MusicHoard"); .expect("failed to initialise MusicHoard");
opt.category.handle(&mut music_hoard); opt.command.handle(&mut music_hoard);
} }

View File

@ -1,4 +1,7 @@
use std::mem; use std::{
fmt::{self, Display},
mem,
};
use crate::core::collection::{ use crate::core::collection::{
merge::{Merge, MergeSorted, WithId}, merge::{Merge, MergeSorted, WithId},
@ -30,25 +33,15 @@ pub struct AlbumId {
// There are crates for handling dates, but we don't need much complexity beyond year-month-day. // There are crates for handling dates, but we don't need much complexity beyond year-month-day.
/// The album's release date. /// The album's release date.
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] #[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumDate { pub struct AlbumDate {
pub year: u32, pub year: u32,
pub month: AlbumMonth, pub month: AlbumMonth,
pub day: u8, pub day: u8,
} }
impl Default for AlbumDate {
fn default() -> Self {
AlbumDate {
year: 0,
month: AlbumMonth::None,
day: 0,
}
}
}
/// The album's sequence to determine order when two or more albums have the same release date. /// The album's sequence to determine order when two or more albums have the same release date.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumSeq(pub u8); pub struct AlbumSeq(pub u8);
#[repr(u8)] #[repr(u8)]
@ -69,6 +62,12 @@ pub enum AlbumMonth {
December = 12, December = 12,
} }
impl Default for AlbumMonth {
fn default() -> Self {
AlbumMonth::None
}
}
impl From<u8> for AlbumMonth { impl From<u8> for AlbumMonth {
fn from(value: u8) -> Self { fn from(value: u8) -> Self {
match value { match value {
@ -93,6 +92,14 @@ impl Album {
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) { pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
(&self.date, &self.seq, &self.id) (&self.date, &self.seq, &self.id)
} }
pub fn set_seq(&mut self, seq: AlbumSeq) {
self.seq = seq;
}
pub fn clear_seq(&mut self) {
self.seq = AlbumSeq::default();
}
} }
impl PartialOrd for Album { impl PartialOrd for Album {
@ -116,6 +123,24 @@ impl Merge for Album {
} }
} }
impl AsRef<AlbumId> for AlbumId {
fn as_ref(&self) -> &AlbumId {
self
}
}
impl AlbumId {
pub fn new<S: Into<String>>(name: S) -> AlbumId {
AlbumId { title: name.into() }
}
}
impl Display for AlbumId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.title)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::core::testmod::FULL_COLLECTION; use crate::core::testmod::FULL_COLLECTION;

View File

@ -171,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
}) })
} }
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
artist.albums.iter_mut().find(|a| &a.id == album_id)
}
fn get_album_mut_or_err<'a>(
artist: &'a mut Artist,
album_id: &AlbumId,
) -> Result<&'a mut Album, Error> {
Self::get_album_mut(artist, album_id).ok_or_else(|| {
Error::CollectionError(format!(
"album '{}' does not belong to the artist",
album_id
))
})
}
} }
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> { impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
@ -243,38 +259,62 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
Ok(()) Ok(())
} }
fn update_collection<F>(&mut self, func: F) -> Result<(), Error> fn update_collection<FN>(&mut self, func: FN) -> Result<(), Error>
where where
F: FnOnce(&mut Collection), FN: FnOnce(&mut Collection),
{ {
func(&mut self.pre_commit); func(&mut self.pre_commit);
self.commit() self.commit()
} }
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>( fn update_artist_and<ID: AsRef<ArtistId>, FNARTIST, FNCOLL>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
f1: F1, fn_artist: FNARTIST,
f2: F2, fn_collection: FNCOLL,
) -> Result<(), Error> ) -> Result<(), Error>
where where
F1: FnOnce(&mut Artist), FNARTIST: FnOnce(&mut Artist),
F2: FnOnce(&mut Collection), FNCOLL: FnOnce(&mut Collection),
{ {
f1(Self::get_artist_mut_or_err( fn_artist(Self::get_artist_mut_or_err(
&mut self.pre_commit, &mut self.pre_commit,
artist_id.as_ref(), artist_id.as_ref(),
)?); )?);
self.update_collection(f2) self.update_collection(fn_collection)
} }
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error> fn update_artist<ID: AsRef<ArtistId>, FN>(
&mut self,
artist_id: ID,
func: FN,
) -> Result<(), Error>
where where
F: FnOnce(&mut Artist), FN: FnOnce(&mut Artist),
{ {
self.update_artist_and(artist_id, func, |_| {}) self.update_artist_and(artist_id, func, |_| {})
} }
fn update_album_and<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>, FNALBUM, FNARTIST, FNCOLL>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
fn_album: FNALBUM,
fn_artist: FNARTIST,
fn_collection: FNCOLL,
) -> Result<(), Error>
where
FNALBUM: FnOnce(&mut Album),
FNARTIST: FnOnce(&mut Artist),
FNCOLL: FnOnce(&mut Collection),
{
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id.as_ref())?;
let album = Self::get_album_mut_or_err(artist, album_id.as_ref())?;
fn_album(album);
fn_artist(artist);
self.update_collection(fn_collection)
}
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> { pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
let artist_id: ArtistId = artist_id.into(); let artist_id: ArtistId = artist_id.into();
@ -315,7 +355,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
) )
} }
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn set_artist_musicbrainz<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
url: S, url: S,
@ -324,14 +364,14 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url)) self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
} }
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>( pub fn clear_artist_musicbrainz<ID: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url()) self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
} }
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>( pub fn add_to_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
property: S, property: S,
@ -340,7 +380,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.add_to_property(property, values)) self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
} }
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn remove_from_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
property: S, property: S,
@ -351,7 +391,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
}) })
} }
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>( pub fn set_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
property: S, property: S,
@ -360,13 +400,42 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.set_property(property, values)) self.update_artist(artist_id, |artist| artist.set_property(property, values))
} }
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>( pub fn clear_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self, &mut self,
artist_id: ID, artist_id: ID,
property: S, property: S,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_property(property)) self.update_artist(artist_id, |artist| artist.clear_property(property))
} }
pub fn set_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
seq: u8,
) -> Result<(), Error> {
self.update_album_and(
artist_id,
album_id,
|album| album.set_seq(AlbumSeq(seq)),
|artist| artist.albums.sort_unstable(),
|_| {},
)
}
pub fn clear_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
) -> Result<(), Error> {
self.update_album_and(
artist_id,
album_id,
|album| album.clear_seq(),
|artist| artist.albums.sort_unstable(),
|_| {},
)
}
} }
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> { impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
@ -518,7 +587,7 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let actual_err = music_hoard let actual_err = music_hoard
.set_musicbrainz_url(&artist_id, MUSICBUTLER) .set_artist_musicbrainz(&artist_id, MUSICBUTLER)
.unwrap_err(); .unwrap_err();
let expected_err = Error::CollectionError(format!( let expected_err = Error::CollectionError(format!(
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}" "an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
@ -544,23 +613,23 @@ mod tests {
// Setting a URL on an artist not in the collection is an error. // Setting a URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
.is_err()); .is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Setting a URL on an artist. // Setting a URL on an artist.
assert!(music_hoard assert!(music_hoard
.set_musicbrainz_url(&artist_id, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
.is_ok()); .is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs on an artist that does not exist is an error. // Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs. // Clearing URLs.
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
_ = expected.take(); _ = expected.take();
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].musicbrainz, expected);
} }
@ -582,13 +651,13 @@ mod tests {
// Adding URLs to an artist not in the collection is an error. // Adding URLs to an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
// Adding mutliple URLs without clashes. // Adding mutliple URLs without clashes.
assert!(music_hoard assert!(music_hoard
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok()); .is_ok());
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
@ -599,7 +668,7 @@ mod tests {
// Removing URLs from an artist not in the collection is an error. // Removing URLs from an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert_eq!( assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"), music_hoard.collection[0].properties.get("MusicButler"),
@ -608,7 +677,11 @@ mod tests {
// Removing multiple URLs without clashes. // Removing multiple URLs without clashes.
assert!(music_hoard assert!(music_hoard
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .remove_from_artist_property(
&artist_id,
"MusicButler",
vec![MUSICBUTLER, MUSICBUTLER_2]
)
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
@ -631,13 +704,13 @@ mod tests {
// Seting URL on an artist not in the collection is an error. // Seting URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());
// Set URLs. // Set URLs.
assert!(music_hoard assert!(music_hoard
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok()); .is_ok());
expected.clear(); expected.clear();
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
@ -649,12 +722,12 @@ mod tests {
// Clearing URLs on an artist that does not exist is an error. // Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard assert!(music_hoard
.clear_property(&artist_id_2, "MusicButler") .clear_artist_property(&artist_id_2, "MusicButler")
.is_err()); .is_err());
// Clear URLs. // Clear URLs.
assert!(music_hoard assert!(music_hoard
.clear_property(&artist_id, "MusicButler") .clear_artist_property(&artist_id, "MusicButler")
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].properties.is_empty());

View File

@ -75,7 +75,12 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
let ui = Ui; let ui = Ui;
// Run the TUI application. // Run the TUI application.
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); let result = Tui::run(terminal, app, ui, handler, listener);
if let Err(tui::Error::ListenerPanic(err)) = result {
std::panic::resume_unwind(err);
} else {
result.expect("failed to run tui")
};
} }
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) { fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {

View File

@ -11,12 +11,16 @@ pub use handler::EventHandler;
pub use listener::EventListener; pub use listener::EventListener;
pub use ui::Ui; pub use ui::Ui;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::{
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; event::{DisableMouseCapture, EnableMouseCapture},
use ratatui::backend::Backend; terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
use ratatui::Terminal; };
use std::io; use ratatui::{backend::Backend, Terminal};
use std::marker::PhantomData; use std::{
marker::PhantomData,
ptr,
{any::Any, io},
};
use crate::tui::{ use crate::tui::{
app::{IAppAccess, IAppInteract}, app::{IAppAccess, IAppInteract},
@ -26,11 +30,32 @@ use crate::tui::{
ui::IUi, ui::IUi,
}; };
#[derive(Debug, PartialEq, Eq)] #[derive(Debug)]
pub enum Error { pub enum Error {
Io(String), Io(String),
Event(String), Event(String),
ListenerPanic, ListenerPanic(Box<dyn Any + Send>),
}
impl Eq for Error {}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match self {
Error::Io(this) => match other {
Error::Io(other) => this == other,
_ => false,
},
Error::Event(this) => match other {
Error::Event(other) => this == other,
_ => false,
},
Error::ListenerPanic(this) => match other {
Error::ListenerPanic(other) => ptr::eq(this.as_ref(), other.as_ref()),
_ => false,
},
}
}
} }
impl From<io::Error> for Error { impl From<io::Error> for Error {
@ -114,7 +139,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs // Calling std::panic::resume_unwind(err) as recommended by the Rust docs
// will not produce an error message. The panic error message is printed at // will not produce an error message. The panic error message is printed at
// the location of the panic which at the time is hidden by the TUI. // the location of the panic which at the time is hidden by the TUI.
Err(_) => return Err(Error::ListenerPanic), // Therefore, propagate the error for the caller to resume unwinding.
Err(panic) => return Err(Error::ListenerPanic(panic)),
} }
} }
@ -301,14 +327,14 @@ mod tests {
let result = Tui::main(terminal, app, ui, handler, listener); let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.unwrap_err(), Error::ListenerPanic); assert!(matches!(result.unwrap_err(), Error::ListenerPanic(_)));
} }
#[test] #[test]
fn errors() { fn errors() {
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
let event_err: Error = EventError::Recv.into(); let event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic; let listener_err = Error::ListenerPanic(Box::new("hello"));
assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty()); assert!(!format!("{:?}", event_err).is_empty());