diff --git a/src/collection/mod.rs b/src/collection/mod.rs deleted file mode 100644 index b75f4a1..0000000 --- a/src/collection/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Module for managing the music collection, i.e. "The Music Hoard". - -use std::fmt; - -use crate::{ - database::{self, Database}, - library::{self, Library, Query}, - Artist, -}; - -/// The collection type. -pub type Collection = Vec; - -/// Error type for collection manager. -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - /// The [`CollectionManager`] failed to read/write from/to the library. - LibraryError(String), - /// The [`CollectionManager`] failed to read/write from/to the database. - DatabaseError(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), - Self::DatabaseError(ref s) => { - write!(f, "failed to read/write from/to the database: {s}") - } - } - } -} - -impl From for Error { - fn from(err: library::Error) -> Error { - Error::LibraryError(err.to_string()) - } -} - -impl From for Error { - fn from(err: database::Error) -> Error { - Error::DatabaseError(err.to_string()) - } -} - -pub trait CollectionManager { - /// Rescan the library and integrate any updates into the collection. - fn rescan_library(&mut self) -> Result<(), Error>; - - /// Save the collection state to the database. - fn save_to_database(&mut self) -> Result<(), Error>; - - /// Get the current collection. - fn get_collection(&self) -> &Collection; -} - -/// The collection manager. It is responsible for pulling information from both the library and the -/// database, ensuring its consistent and writing back any changes. -pub struct MhCollectionManager { - library: LIB, - database: DB, - collection: Collection, -} - -impl MhCollectionManager { - /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. - pub fn new(library: LIB, database: DB) -> Self { - MhCollectionManager { - library, - database, - collection: vec![], - } - } -} - -impl CollectionManager for MhCollectionManager { - fn rescan_library(&mut self) -> Result<(), Error> { - self.collection = self.library.list(&Query::new())?; - Ok(()) - } - - fn save_to_database(&mut self) -> Result<(), Error> { - self.database.write(&self.collection)?; - Ok(()) - } - - fn get_collection(&self) -> &Collection { - &self.collection - } -} - -#[cfg(test)] -mod tests { - use mockall::predicate; - - use crate::{ - collection::Collection, - database::{self, MockDatabase}, - library::{self, MockLibrary, Query}, - tests::COLLECTION, - }; - - use super::{CollectionManager, Error, MhCollectionManager}; - - #[test] - fn read_get_write() { - let mut library = MockLibrary::new(); - let mut database = MockDatabase::new(); - - let library_input = Query::new(); - let library_result = Ok(COLLECTION.to_owned()); - - let database_input = COLLECTION.to_owned(); - let database_result = Ok(()); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - database - .expect_write() - .with(predicate::eq(database_input)) - .times(1) - .return_once(|_: &Collection| database_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - collection_manager.rescan_library().unwrap(); - assert_eq!(collection_manager.get_collection(), &*COLLECTION); - collection_manager.save_to_database().unwrap(); - } - - #[test] - fn library_error() { - let mut library = MockLibrary::new(); - let database = MockDatabase::new(); - - let library_result = Err(library::Error::Invalid(String::from("invalid data"))); - - library - .expect_list() - .times(1) - .return_once(|_| library_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - let actual_err = collection_manager.rescan_library().unwrap_err(); - let expected_err = - Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); - } - - #[test] - fn database_error() { - let library = MockLibrary::new(); - let mut database = MockDatabase::new(); - - let database_result = Err(database::Error::IoError(String::from("I/O error"))); - - database - .expect_write() - .times(1) - .return_once(|_: &Collection| database_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - let actual_err = collection_manager.save_to_database().unwrap_err(); - let expected_err = - Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); - } -} diff --git a/src/lib.rs b/src/lib.rs index 06a807b..586f3b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,15 @@ //! MusicHoard - a music collection manager. -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -pub mod collection; pub mod database; pub mod library; +use std::fmt; + +use database::Database; +use library::{Library, Query}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; @@ -53,15 +56,160 @@ pub struct Artist { pub albums: Vec, } +/// The collection type. Currently, a collection is a list of artists. +pub type Collection = Vec; + +/// Error type for `musichoard`. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// The [`MusicHoard`] failed to read/write from/to the library. + LibraryError(String), + /// The [`MusicHoard`] failed to read/write from/to the database. + DatabaseError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), + Self::DatabaseError(ref s) => { + write!(f, "failed to read/write from/to the database: {s}") + } + } + } +} + +impl From for Error { + fn from(err: library::Error) -> Error { + Error::LibraryError(err.to_string()) + } +} + +impl From for Error { + fn from(err: database::Error) -> Error { + Error::DatabaseError(err.to_string()) + } +} + +/// The Music Hoard. It is responsible for pulling information from both the library and the +/// database, ensuring its consistent and writing back any changes. +pub struct MusicHoard { + library: LIB, + database: DB, + collection: Collection, +} + +impl MusicHoard { + /// Create a new [`MusicHoard`] with the provided [`Library`] and [`Database`]. + pub fn new(library: LIB, database: DB) -> Self { + MusicHoard { + library, + database, + collection: vec![], + } + } + + pub fn rescan_library(&mut self) -> Result<(), Error> { + self.collection = self.library.list(&Query::new())?; + Ok(()) + } + + pub fn save_to_database(&mut self) -> Result<(), Error> { + self.database.write(&self.collection)?; + Ok(()) + } + + pub fn get_collection(&self) -> &Collection { + &self.collection + } +} + #[cfg(test)] #[macro_use] mod testlib; #[cfg(test)] mod tests { + use mockall::predicate; use once_cell::sync::Lazy; + use crate::{database::MockDatabase, library::MockLibrary}; + use super::*; pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); + + #[test] + fn read_get_write() { + let mut library = MockLibrary::new(); + let mut database = MockDatabase::new(); + + let library_input = Query::new(); + let library_result = Ok(COLLECTION.to_owned()); + + let database_input = COLLECTION.to_owned(); + let database_result = Ok(()); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + database + .expect_write() + .with(predicate::eq(database_input)) + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoard::new(library, database); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*COLLECTION); + music_hoard.save_to_database().unwrap(); + } + + #[test] + fn library_error() { + let mut library = MockLibrary::new(); + let database = MockDatabase::new(); + + let library_result = Err(library::Error::Invalid(String::from("invalid data"))); + + library + .expect_list() + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoard::new(library, database); + + let actual_err = music_hoard.rescan_library().unwrap_err(); + let expected_err = + Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } + + #[test] + fn database_error() { + let library = MockLibrary::new(); + let mut database = MockDatabase::new(); + + let database_result = Err(database::Error::IoError(String::from("I/O error"))); + + database + .expect_write() + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoard::new(library, database); + + let actual_err = music_hoard.save_to_database().unwrap_err(); + let expected_err = + Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } } diff --git a/src/main.rs b/src/main.rs index 5e9c426..ff4dd14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use structopt::StructOpt; use musichoard::{ - collection::MhCollectionManager, database::{ json::{backend::JsonDatabaseFileBackend, JsonDatabase}, Database, @@ -17,6 +16,7 @@ use musichoard::{ }, Library, }, + MusicHoard, }; mod tui; @@ -40,7 +40,7 @@ struct Opt { } fn with(lib: LIB, db: DB) { - let collection_manager = MhCollectionManager::new(lib, db); + let music_hoard = MusicHoard::new(lib, db); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); @@ -50,7 +50,7 @@ fn with(lib: LIB, db: DB) { let listener = TuiEventListener::new(channel.sender()); let handler = TuiEventHandler::new(channel.receiver()); - let ui = MhUi::new(collection_manager).expect("failed to initialise ui"); + let ui = MhUi::new(music_hoard).expect("failed to initialise ui"); // Run the TUI application. Tui::run(terminal, ui, handler, listener).expect("failed to run tui"); @@ -85,21 +85,9 @@ mod testlib; #[cfg(test)] mod tests { - use mockall::mock; use once_cell::sync::Lazy; - use musichoard::collection::{self, Collection, CollectionManager}; use musichoard::*; pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); - - mock! { - pub CollectionManager {} - - impl CollectionManager for CollectionManager { - fn rescan_library(&mut self) -> Result<(), collection::Error>; - fn save_to_database(&mut self) -> Result<(), collection::Error>; - fn get_collection(&self) -> &Collection; - } - } } diff --git a/src/tui/lib.rs b/src/tui/lib.rs new file mode 100644 index 0000000..754bc85 --- /dev/null +++ b/src/tui/lib.rs @@ -0,0 +1,21 @@ +use musichoard::{MusicHoard, library::Library, database::Database, Collection}; + +use super::{ui::IMusicHoard, Error}; + +impl From for Error { + fn from(err: musichoard::Error) -> Error { + Error::Collection(err.to_string()) + } +} + +// GRCOV_EXCL_START +impl IMusicHoard for MusicHoard { + fn rescan_library(&mut self) -> Result<(), Error> { + Ok(MusicHoard::rescan_library(self)?) + } + + fn get_collection(&self) -> &Collection { + MusicHoard::get_collection(self) + } +} +// GRCOV_EXCL_STOP diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 50478b6..66ff6dc 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,16 +1,17 @@ -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use musichoard::collection; -use ratatui::backend::Backend; -use ratatui::Terminal; -use std::io; -use std::marker::PhantomData; - pub mod event; pub mod handler; pub mod listener; pub mod ui; +mod lib; + +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use ratatui::backend::Backend; +use ratatui::Terminal; +use std::io; +use std::marker::PhantomData; + use self::event::EventError; use self::handler::EventHandler; use self::listener::EventListener; @@ -24,12 +25,6 @@ pub enum Error { ListenerPanic, } -impl From for Error { - fn from(err: collection::Error) -> Error { - Error::Collection(err.to_string()) - } -} - impl From for Error { fn from(err: io::Error) -> Error { Error::Io(err.to_string()) @@ -160,16 +155,16 @@ impl Tui { mod tests { use std::{io, thread}; - use musichoard::collection::{self, Collection}; + use musichoard::Collection; use ratatui::{backend::TestBackend, Terminal}; - use crate::tests::{MockCollectionManager, COLLECTION}; + use crate::tests::COLLECTION; use super::{ event::EventError, handler::MockEventHandler, listener::MockEventListener, - ui::{MhUi, Ui}, + ui::{MhUi, MockIMusicHoard, Ui}, Error, Tui, }; @@ -178,17 +173,13 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn ui(collection: Collection) -> MhUi { - let mut collection_manager = MockCollectionManager::new(); + pub fn ui(collection: Collection) -> MhUi { + let mut music_hoard = MockIMusicHoard::new(); - collection_manager - .expect_rescan_library() - .returning(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); + music_hoard.expect_rescan_library().returning(|| Ok(())); + music_hoard.expect_get_collection().return_const(collection); - MhUi::new(collection_manager).unwrap() + MhUi::new(music_hoard).unwrap() } fn listener() -> MockEventListener { @@ -202,11 +193,11 @@ mod tests { listener } - fn handler() -> MockEventHandler> { + fn handler() -> MockEventHandler> { let mut handler = MockEventHandler::new(); handler .expect_handle_next_event() - .return_once(|ui: &mut MhUi| { + .return_once(|ui: &mut MhUi| { ui.quit(); Ok(()) }); @@ -292,7 +283,7 @@ mod tests { #[test] fn errors() { - let collection_err: Error = collection::Error::DatabaseError(String::from("")).into(); + let collection_err: Error = musichoard::Error::DatabaseError(String::from("")).into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let event_err: Error = EventError::Recv.into(); let listener_err = Error::ListenerPanic; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index cb13a98..0b66f27 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,7 +1,4 @@ -use musichoard::{ - collection::{Collection, CollectionManager}, - Album, Artist, Track, TrackFormat, -}; +use musichoard::{Album, Artist, Collection, Track, TrackFormat}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -10,8 +7,17 @@ use ratatui::{ Frame, }; +#[cfg(test)] +use mockall::automock; + use super::Error; +#[cfg_attr(test, automock)] +pub trait IMusicHoard { + fn rescan_library(&mut self) -> Result<(), Error>; + fn get_collection(&self) -> &Collection; +} + struct TrackSelection { state: ListState, } @@ -239,8 +245,8 @@ impl Selection { } } -pub struct MhUi { - collection_manager: CM, +pub struct MhUi { + music_hoard: MH, selection: Selection, running: bool, } @@ -428,12 +434,12 @@ impl<'a, 'b> TrackState<'a, 'b> { } } -impl MhUi { - pub fn new(mut collection_manager: CM) -> Result { - collection_manager.rescan_library()?; - let selection = Selection::new(Some(collection_manager.get_collection())); +impl MhUi { + pub fn new(mut music_hoard: MH) -> Result { + music_hoard.rescan_library()?; + let selection = Selection::new(Some(music_hoard.get_collection())); Ok(MhUi { - collection_manager, + music_hoard, selection, running: true, }) @@ -525,7 +531,7 @@ pub trait Ui { fn render(&mut self, frame: &mut Frame<'_, B>); } -impl Ui for MhUi { +impl Ui for MhUi { fn is_running(&self) -> bool { self.running } @@ -544,19 +550,19 @@ impl Ui for MhUi { fn increment_selection(&mut self) { self.selection - .increment_selection(self.collection_manager.get_collection()); + .increment_selection(self.music_hoard.get_collection()); } fn decrement_selection(&mut self) { self.selection - .decrement_selection(self.collection_manager.get_collection()); + .decrement_selection(self.music_hoard.get_collection()); } fn render(&mut self, frame: &mut Frame<'_, B>) { let active = self.selection.active; let areas = FrameArea::new(frame.size()); - let artists = self.collection_manager.get_collection(); + let artists = self.music_hoard.get_collection(); let artist_selection = &mut self.selection.artist; let artist_state = ArtistState::new( active == Category::Artist, @@ -600,7 +606,7 @@ impl Ui for MhUi { #[cfg(test)] mod tests { - use crate::tests::{MockCollectionManager, COLLECTION}; + use crate::tests::COLLECTION; use crate::tui::tests::{terminal, ui}; use super::*; @@ -759,17 +765,17 @@ mod tests { #[test] fn ui_running() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut ui = MhUi::new(collection_manager).unwrap(); + let mut ui = MhUi::new(music_hoard).unwrap(); assert!(ui.is_running()); ui.quit(); @@ -778,17 +784,17 @@ mod tests { #[test] fn ui_modifiers() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut ui = MhUi::new(collection_manager).unwrap(); + let mut ui = MhUi::new(music_hoard).unwrap(); assert!(ui.is_running()); assert_eq!(ui.selection.active, Category::Artist); @@ -877,19 +883,19 @@ mod tests { #[test] fn app_no_tracks() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = MhUi::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -915,19 +921,19 @@ mod tests { #[test] fn app_no_albums() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = MhUi::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -966,18 +972,18 @@ mod tests { #[test] fn app_no_artists() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let collection = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = MhUi::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist);